Commit e501cf51a0

Andrew Kelley <andrew@ziglang.org>
2024-11-01 05:56:10
link.File.Wasm: unify the string tables
Before, the wasm struct had a string table, the ZigObject had a string table, and each Object had a string table. Now there is just the one. This makes for more efficient use of memory and simplifies logic, particularly with regards to linker state serialization. This commit additionally adds significantly more integer type safety.
1 parent bf9978a
src/link/Wasm/Archive.zig
@@ -173,7 +173,7 @@ fn parseNameTable(gpa: Allocator, reader: anytype) ![]const u8 {
 
 /// From a given file offset, starts reading for a file header.
 /// When found, parses the object file into an `Object` and returns it.
-pub fn parseObject(archive: Archive, wasm: *const Wasm, file_contents: []const u8, path: Path) !Object {
+pub fn parseObject(archive: Archive, wasm: *Wasm, file_contents: []const u8, path: Path) !Object {
     var fbs = std.io.fixedBufferStream(file_contents);
     const header = try fbs.reader().readStruct(Header);
 
src/link/Wasm/Object.zig
@@ -63,10 +63,6 @@ comdat_info: []const Wasm.Comdat = &.{},
 /// Represents non-synthetic sections that can essentially be mem-cpy'd into place
 /// after performing relocations.
 relocatable_data: std.AutoHashMapUnmanaged(RelocatableData.Tag, []RelocatableData) = .empty,
-/// String table for all strings required by the object file, such as symbol names,
-/// 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.
@@ -126,7 +122,7 @@ pub const RelocatableData = struct {
 /// When a max size is given, will only parse up to the given size,
 /// else will read until the end of the file.
 pub fn create(
-    wasm: *const Wasm,
+    wasm: *Wasm,
     file_contents: []const u8,
     path: Path,
     archive_member_name: ?[]const u8,
@@ -187,7 +183,6 @@ pub fn deinit(object: *Object, gpa: Allocator) void {
         }
     }
     object.relocatable_data.deinit(gpa);
-    object.string_table.deinit(gpa);
     object.* = undefined;
 }
 
@@ -242,9 +237,9 @@ fn checkLegacyIndirectFunctionTable(object: *Object, wasm: *const Wasm) !?Symbol
         }
     } else unreachable;
 
-    if (!std.mem.eql(u8, object.string_table.get(table_import.name), "__indirect_function_table")) {
+    if (table_import.name != wasm.preloaded_strings.__indirect_function_table) {
         return diags.failParse(object.path, "non-indirect function table import '{s}' is missing a corresponding symbol", .{
-            object.string_table.get(table_import.name),
+            wasm.stringSlice(table_import.name),
         });
     }
 
@@ -264,10 +259,12 @@ const Parser = struct {
     reader: std.io.FixedBufferStream([]const u8),
     /// Object file we're building
     object: *Object,
-    /// Read-only reference to the WebAssembly linker
-    wasm: *const Wasm,
+    /// Mutable so that the string table can be modified.
+    wasm: *Wasm,
 
     fn parseObject(parser: *Parser, gpa: Allocator) anyerror!void {
+        const wasm = parser.wasm;
+
         {
             var magic_bytes: [4]u8 = undefined;
             try parser.reader.reader().readNoEof(&magic_bytes);
@@ -316,7 +313,7 @@ const Parser = struct {
                             .type = .custom,
                             .data = debug_content.ptr,
                             .size = debug_size,
-                            .index = try parser.object.string_table.put(gpa, name),
+                            .index = @intFromEnum(try wasm.internString(name)),
                             .offset = 0, // debug sections only contain 1 entry, so no need to calculate offset
                             .section_index = section_index,
                         });
@@ -375,8 +372,8 @@ const Parser = struct {
                         };
 
                         import.* = .{
-                            .module_name = try parser.object.string_table.put(gpa, module_name),
-                            .name = try parser.object.string_table.put(gpa, name),
+                            .module_name = try wasm.internString(module_name),
+                            .name = try wasm.internString(name),
                             .kind = kind_value,
                         };
                     }
@@ -422,7 +419,7 @@ const Parser = struct {
                         defer gpa.free(name);
                         try reader.readNoEof(name);
                         exp.* = .{
-                            .name = try parser.object.string_table.put(gpa, name),
+                            .name = try wasm.internString(name),
                             .kind = try readEnum(std.wasm.ExternalKind, reader),
                             .index = try readLeb(u32, reader),
                         };
@@ -587,6 +584,7 @@ const Parser = struct {
     /// `parser` is used to provide access to other sections that may be needed,
     /// such as access to the `import` section to find the name of a symbol.
     fn parseSubsection(parser: *Parser, gpa: Allocator, reader: anytype) !void {
+        const wasm = parser.wasm;
         const sub_type = try leb.readUleb128(u8, reader);
         log.debug("Found subsection: {s}", .{@tagName(@as(Wasm.SubsectionType, @enumFromInt(sub_type)))});
         const payload_len = try leb.readUleb128(u32, reader);
@@ -680,7 +678,7 @@ const Parser = struct {
                     symbol.* = try parser.parseSymbol(gpa, reader);
                     log.debug("Found symbol: type({s}) name({s}) flags(0b{b:0>8})", .{
                         @tagName(symbol.tag),
-                        parser.object.string_table.get(symbol.name),
+                        wasm.stringSlice(symbol.name),
                         symbol.flags,
                     });
                 }
@@ -697,15 +695,18 @@ const Parser = struct {
                 if (parser.object.relocatable_data.get(.custom)) |custom_sections| {
                     for (custom_sections) |*data| {
                         if (!data.represented) {
+                            const name = wasm.castToString(data.index);
                             try symbols.append(.{
-                                .name = data.index,
+                                .name = name,
                                 .flags = @intFromEnum(Symbol.Flag.WASM_SYM_BINDING_LOCAL),
                                 .tag = .section,
                                 .virtual_address = 0,
                                 .index = data.section_index,
                             });
                             data.represented = true;
-                            log.debug("Created synthetic custom section symbol for '{s}'", .{parser.object.string_table.get(data.index)});
+                            log.debug("Created synthetic custom section symbol for '{s}'", .{
+                                wasm.stringSlice(name),
+                            });
                         }
                     }
                 }
@@ -719,7 +720,8 @@ const Parser = struct {
     /// requires access to `Object` to find the name of a symbol when it's
     /// an import and flag `WASM_SYM_EXPLICIT_NAME` is not set.
     fn parseSymbol(parser: *Parser, gpa: Allocator, reader: anytype) !Symbol {
-        const tag = @as(Symbol.Tag, @enumFromInt(try leb.readUleb128(u8, reader)));
+        const wasm = parser.wasm;
+        const tag: Symbol.Tag = @enumFromInt(try leb.readUleb128(u8, reader));
         const flags = try leb.readUleb128(u32, reader);
         var symbol: Symbol = .{
             .flags = flags,
@@ -735,7 +737,7 @@ const Parser = struct {
                 const name = try gpa.alloc(u8, name_len);
                 defer gpa.free(name);
                 try reader.readNoEof(name);
-                symbol.name = try parser.object.string_table.put(gpa, name);
+                symbol.name = try wasm.internString(name);
 
                 // Data symbols only have the following fields if the symbol is defined
                 if (symbol.isDefined()) {
@@ -750,7 +752,7 @@ const Parser = struct {
                 const section_data = parser.object.relocatable_data.get(.custom).?;
                 for (section_data) |*data| {
                     if (data.section_index == symbol.index) {
-                        symbol.name = data.index;
+                        symbol.name = wasm.castToString(data.index);
                         data.represented = true;
                         break;
                     }
@@ -765,7 +767,7 @@ const Parser = struct {
                     const name = try gpa.alloc(u8, name_len);
                     defer gpa.free(name);
                     try reader.readNoEof(name);
-                    break :name try parser.object.string_table.put(gpa, name);
+                    break :name try wasm.internString(name);
                 } else parser.object.findImport(symbol).name;
             },
         }
src/link/Wasm/Symbol.zig
@@ -8,8 +8,8 @@
 /// Can contain any of the flags defined in `Flag`
 flags: u32,
 /// Symbol name, when the symbol is undefined the name will be taken from the import.
-/// Note: This is an index into the string table.
-name: u32,
+/// Note: This is an index into the wasm string table.
+name: wasm.String,
 /// Index into the list of objects based on set `tag`
 /// NOTE: This will be set to `undefined` when `tag` is `data`
 /// and the symbol is undefined.
@@ -207,3 +207,4 @@ pub fn format(symbol: Symbol, comptime fmt: []const u8, options: std.fmt.FormatO
 
 const std = @import("std");
 const Symbol = @This();
+const wasm = @import("../Wasm.zig");
src/link/Wasm/ZigObject.zig
@@ -23,16 +23,14 @@ globals: std.ArrayListUnmanaged(std.wasm.Global) = .empty,
 atom_types: std.AutoHashMapUnmanaged(Atom.Index, u32) = .empty,
 /// List of all symbols generated by Zig code.
 symbols: std.ArrayListUnmanaged(Symbol) = .empty,
-/// Map from symbol name offset to their index into the `symbols` list.
-global_syms: std.AutoHashMapUnmanaged(u32, Symbol.Index) = .empty,
+/// Map from symbol name to their index into the `symbols` list.
+global_syms: std.AutoHashMapUnmanaged(Wasm.String, Symbol.Index) = .empty,
 /// List of symbol indexes which are free to be used.
 symbols_free_list: std.ArrayListUnmanaged(Symbol.Index) = .empty,
 /// Extra metadata about the linking section, such as alignment of segments and their name.
 segment_info: std.ArrayListUnmanaged(Wasm.NamedSegment) = .empty,
 /// List of indexes which contain a free slot in the `segment_info` list.
 segment_free_list: std.ArrayListUnmanaged(u32) = .empty,
-/// File encapsulated string table, used to deduplicate strings within the generated file.
-string_table: StringTable = .{},
 /// Map for storing anonymous declarations. Each anonymous decl maps to its Atom's index.
 uavs: std.AutoArrayHashMapUnmanaged(InternPool.Index, Atom.Index) = .empty,
 /// List of atom indexes of functions that are generated by the backend.
@@ -88,13 +86,9 @@ const NavInfo = struct {
     atom: Atom.Index = .null,
     exports: std.ArrayListUnmanaged(Symbol.Index) = .empty,
 
-    fn @"export"(ni: NavInfo, zig_object: *const ZigObject, name: []const u8) ?Symbol.Index {
+    fn @"export"(ni: NavInfo, zo: *const ZigObject, name: Wasm.String) ?Symbol.Index {
         for (ni.exports.items) |sym_index| {
-            const sym_name_index = zig_object.symbol(sym_index).name;
-            const sym_name = zig_object.string_table.getAssumeExists(sym_name_index);
-            if (std.mem.eql(u8, name, sym_name)) {
-                return sym_index;
-            }
+            if (zo.symbol(sym_index).name == name) return sym_index;
         }
         return null;
     }
@@ -126,14 +120,14 @@ pub fn init(zig_object: *ZigObject, wasm: *Wasm) !void {
 
 fn createStackPointer(zig_object: *ZigObject, wasm: *Wasm) !void {
     const gpa = wasm.base.comp.gpa;
-    const sym_index = try zig_object.getGlobalSymbol(gpa, "__stack_pointer");
+    const sym_index = try zig_object.getGlobalSymbol(gpa, wasm.preloaded_strings.__stack_pointer);
     const sym = zig_object.symbol(sym_index);
     sym.index = zig_object.imported_globals_count;
     sym.tag = .global;
     const is_wasm32 = wasm.base.comp.root_mod.resolved_target.result.cpu.arch == .wasm32;
     try zig_object.imports.putNoClobber(gpa, sym_index, .{
         .name = sym.name,
-        .module_name = try zig_object.string_table.insert(gpa, wasm.host_name),
+        .module_name = wasm.host_name,
         .kind = .{ .global = .{ .valtype = if (is_wasm32) .i32 else .i64, .mutable = true } },
     });
     zig_object.imported_globals_count += 1;
@@ -174,7 +168,7 @@ pub fn deinit(zig_object: *ZigObject, wasm: *Wasm) void {
             atom.deinit(gpa);
         }
     }
-    if (zig_object.findGlobalSymbol("__zig_errors_len")) |sym_index| {
+    if (zig_object.global_syms.get(wasm.preloaded_strings.__zig_errors_len)) |sym_index| {
         const atom_index = wasm.symbol_atom.get(.{ .file = .zig_object, .index = sym_index }).?;
         wasm.getAtomPtr(atom_index).deinit(gpa);
     }
@@ -206,7 +200,6 @@ pub fn deinit(zig_object: *ZigObject, wasm: *Wasm) void {
     zig_object.segment_info.deinit(gpa);
     zig_object.segment_free_list.deinit(gpa);
 
-    zig_object.string_table.deinit(gpa);
     if (zig_object.dwarf) |*dwarf| {
         dwarf.deinit();
     }
@@ -219,7 +212,7 @@ pub fn deinit(zig_object: *ZigObject, wasm: *Wasm) void {
 pub fn allocateSymbol(zig_object: *ZigObject, gpa: std.mem.Allocator) !Symbol.Index {
     try zig_object.symbols.ensureUnusedCapacity(gpa, 1);
     const sym: Symbol = .{
-        .name = std.math.maxInt(u32), // will be set after updateDecl as well as during atom creation for decls
+        .name = undefined, // will be set after updateDecl as well as during atom creation for decls
         .flags = @intFromEnum(Symbol.Flag.WASM_SYM_BINDING_LOCAL),
         .tag = .undefined, // will be set after updateDecl
         .index = std.math.maxInt(u32), // will be set during atom parsing
@@ -345,7 +338,7 @@ fn finishUpdateNav(
     const atom_index = nav_info.atom;
     const atom = wasm.getAtomPtr(atom_index);
     const sym = zig_object.symbol(atom.sym_index);
-    sym.name = try zig_object.string_table.insert(gpa, nav.fqn.toSlice(ip));
+    sym.name = try wasm.internString(nav.fqn.toSlice(ip));
     try atom.code.appendSlice(gpa, code);
     atom.size = @intCast(code.len);
 
@@ -432,7 +425,7 @@ pub fn getOrCreateAtomForNav(
         gop.value_ptr.* = .{ .atom = try wasm.createAtom(sym_index, .zig_object) };
         const nav = ip.getNav(nav_index);
         const sym = zig_object.symbol(sym_index);
-        sym.name = try zig_object.string_table.insert(gpa, nav.fqn.toSlice(ip));
+        sym.name = try wasm.internString(nav.fqn.toSlice(ip));
     }
     return gop.value_ptr.atom;
 }
@@ -500,7 +493,7 @@ fn lowerConst(
         const segment_name = try std.mem.concat(gpa, u8, &.{ ".rodata.", name });
         errdefer gpa.free(segment_name);
         zig_object.symbol(sym_index).* = .{
-            .name = try zig_object.string_table.insert(gpa, name),
+            .name = try wasm.internString(name),
             .flags = @intFromEnum(Symbol.Flag.WASM_SYM_BINDING_LOCAL),
             .tag = .data,
             .index = try zig_object.createDataSegment(
@@ -551,11 +544,10 @@ pub fn getErrorTableSymbol(zig_object: *ZigObject, wasm: *Wasm, pt: Zcu.PerThrea
     const slice_ty = Type.slice_const_u8_sentinel_0;
     atom.alignment = slice_ty.abiAlignment(pt.zcu);
 
-    const sym_name = try zig_object.string_table.insert(gpa, "__zig_err_name_table");
     const segment_name = try gpa.dupe(u8, ".rodata.__zig_err_name_table");
     const sym = zig_object.symbol(sym_index);
     sym.* = .{
-        .name = sym_name,
+        .name = wasm.preloaded_strings.__zig_err_name_table,
         .tag = .data,
         .flags = @intFromEnum(Symbol.Flag.WASM_SYM_BINDING_LOCAL),
         .index = try zig_object.createDataSegment(gpa, segment_name, atom.alignment),
@@ -583,11 +575,10 @@ fn populateErrorNameTable(zig_object: *ZigObject, wasm: *Wasm, tid: Zcu.PerThrea
     const names_atom_index = try wasm.createAtom(names_sym_index, .zig_object);
     const names_atom = wasm.getAtomPtr(names_atom_index);
     names_atom.alignment = .@"1";
-    const sym_name = try zig_object.string_table.insert(gpa, "__zig_err_names");
     const segment_name = try gpa.dupe(u8, ".rodata.__zig_err_names");
     const names_symbol = zig_object.symbol(names_sym_index);
     names_symbol.* = .{
-        .name = sym_name,
+        .name = wasm.preloaded_strings.__zig_err_names,
         .tag = .data,
         .flags = @intFromEnum(Symbol.Flag.WASM_SYM_BINDING_LOCAL),
         .index = try zig_object.createDataSegment(gpa, segment_name, names_atom.alignment),
@@ -661,14 +652,14 @@ pub fn addOrUpdateImport(
     // 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, lib_name.?, "c");
-    const full_name = if (mangle_name) full_name: {
-        break :full_name try std.fmt.allocPrint(gpa, "{s}|{s}", .{ name, lib_name.? });
-    } else name;
+    const mangle_name = if (lib_name) |n| !std.mem.eql(u8, n, "c") else false;
+    const full_name = if (mangle_name)
+        try std.fmt.allocPrint(gpa, "{s}|{s}", .{ name, lib_name.? })
+    else
+        name;
     defer if (mangle_name) gpa.free(full_name);
 
-    const decl_name_index = try zig_object.string_table.insert(gpa, full_name);
+    const decl_name_index = try wasm.internString(full_name);
     const sym: *Symbol = &zig_object.symbols.items[@intFromEnum(symbol_index)];
     sym.setUndefined(true);
     sym.setGlobal(true);
@@ -680,13 +671,11 @@ pub fn addOrUpdateImport(
 
     if (type_index) |ty_index| {
         const gop = try zig_object.imports.getOrPut(gpa, symbol_index);
-        const module_name = if (lib_name) |l_name| l_name else wasm.host_name;
-        if (!gop.found_existing) {
-            zig_object.imported_functions_count += 1;
-        }
+        const module_name = if (lib_name) |n| try wasm.internString(n) else wasm.host_name;
+        if (!gop.found_existing) zig_object.imported_functions_count += 1;
         gop.value_ptr.* = .{
-            .module_name = try zig_object.string_table.insert(gpa, module_name),
-            .name = try zig_object.string_table.insert(gpa, name),
+            .module_name = module_name,
+            .name = try wasm.internString(name),
             .kind = .{ .function = ty_index },
         };
         sym.tag = .function;
@@ -699,8 +688,7 @@ pub fn addOrUpdateImport(
 /// such as an exported or imported symbol.
 /// If the symbol does not yet exist, creates a new one symbol instead
 /// and then returns the index to it.
-pub fn getGlobalSymbol(zig_object: *ZigObject, gpa: std.mem.Allocator, name: []const u8) !Symbol.Index {
-    const name_index = try zig_object.string_table.insert(gpa, name);
+pub fn getGlobalSymbol(zig_object: *ZigObject, gpa: std.mem.Allocator, name_index: Wasm.String) !Symbol.Index {
     const gop = try zig_object.global_syms.getOrPut(gpa, name_index);
     if (gop.found_existing) {
         return gop.value_ptr.*;
@@ -840,7 +828,8 @@ pub fn deleteExport(
         .uav => @panic("TODO: implement Wasm linker code for exporting a constant value"),
     };
     const nav_info = zig_object.navs.getPtr(nav_index) orelse return;
-    if (nav_info.@"export"(zig_object, name.toSlice(&zcu.intern_pool))) |sym_index| {
+    const name_interned = wasm.getExistingString(name.toSlice(&zcu.intern_pool)).?;
+    if (nav_info.@"export"(zig_object, name_interned)) |sym_index| {
         const sym = zig_object.symbol(sym_index);
         nav_info.deleteExport(sym_index);
         std.debug.assert(zig_object.global_syms.remove(sym.name));
@@ -886,14 +875,13 @@ pub fn updateExports(
             continue;
         }
 
-        const export_string = exp.opts.name.toSlice(ip);
-        const sym_index = if (nav_info.@"export"(zig_object, export_string)) |idx| idx else index: {
+        const export_name = try wasm.internString(exp.opts.name.toSlice(ip));
+        const sym_index = if (nav_info.@"export"(zig_object, export_name)) |idx| idx else index: {
             const sym_index = try zig_object.allocateSymbol(gpa);
             try nav_info.appendExport(gpa, sym_index);
             break :index sym_index;
         };
 
-        const export_name = try zig_object.string_table.insert(gpa, export_string);
         const sym = zig_object.symbol(sym_index);
         sym.setGlobal(true);
         sym.setUndefined(false);
@@ -922,7 +910,7 @@ pub fn updateExports(
         if (exp.opts.visibility == .hidden) {
             sym.setFlag(.WASM_SYM_VISIBILITY_HIDDEN);
         }
-        log.debug("  with name '{s}' - {}", .{ export_string, sym });
+        log.debug("  with name '{s}' - {}", .{ wasm.stringSlice(export_name), sym });
         try zig_object.global_syms.put(gpa, export_name, sym_index);
         try wasm.symbol_atom.put(gpa, .{ .file = .zig_object, .index = sym_index }, atom_index);
     }
@@ -1014,7 +1002,7 @@ pub fn putOrGetFuncType(zig_object: *ZigObject, gpa: std.mem.Allocator, func_typ
 /// This will only be generated if the symbol exists.
 fn setupErrorsLen(zig_object: *ZigObject, wasm: *Wasm) !void {
     const gpa = wasm.base.comp.gpa;
-    const sym_index = zig_object.findGlobalSymbol("__zig_errors_len") orelse return;
+    const sym_index = zig_object.global_syms.get(wasm.preloaded_strings.__zig_errors_len) orelse return;
 
     const errors_len = 1 + wasm.base.comp.zcu.?.intern_pool.global_error_set.getNamesFromMainThread().len;
     // overwrite existing atom if it already exists (maybe the error set has increased)
@@ -1045,11 +1033,6 @@ fn setupErrorsLen(zig_object: *ZigObject, wasm: *Wasm) !void {
     try atom.code.writer(gpa).writeInt(u16, @intCast(errors_len), .little);
 }
 
-fn findGlobalSymbol(zig_object: *ZigObject, name: []const u8) ?Symbol.Index {
-    const offset = zig_object.string_table.getOffset(name) orelse return null;
-    return zig_object.global_syms.get(offset);
-}
-
 /// 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
@@ -1082,7 +1065,7 @@ pub fn createDebugSectionForIndex(zig_object: *ZigObject, wasm: *Wasm, index: *?
     const atom = wasm.getAtomPtr(atom_index);
     zig_object.symbols.items[sym_index] = .{
         .tag = .section,
-        .name = try zig_object.string_table.put(gpa, name),
+        .name = try wasm.internString(name),
         .index = 0,
         .flags = @intFromEnum(Symbol.Flag.WASM_SYM_BINDING_LOCAL),
     };
@@ -1197,7 +1180,7 @@ pub fn createFunction(
     const sym_index = try zig_object.allocateSymbol(gpa);
     const sym = zig_object.symbol(sym_index);
     sym.tag = .function;
-    sym.name = try zig_object.string_table.insert(gpa, symbol_name);
+    sym.name = try wasm.internString(symbol_name);
     const type_index = try zig_object.putOrGetFuncType(gpa, func_ty);
     sym.index = try zig_object.appendFunction(gpa, .{ .type_index = type_index });
 
@@ -1244,7 +1227,6 @@ const Dwarf = @import("../Dwarf.zig");
 const InternPool = @import("../../InternPool.zig");
 const Liveness = @import("../../Liveness.zig");
 const Zcu = @import("../../Zcu.zig");
-const StringTable = @import("../StringTable.zig");
 const Symbol = @import("Symbol.zig");
 const Type = @import("../../Type.zig");
 const Value = @import("../../Value.zig");
src/link/Wasm.zig
@@ -36,11 +36,16 @@ const Value = @import("../Value.zig");
 const ZigObject = @import("Wasm/ZigObject.zig");
 
 base: link.File,
+/// Null-terminated strings, indexes have type String and string_table provides
+/// lookup.
+string_bytes: std.ArrayListUnmanaged(u8),
+/// Omitted when serializing linker state.
+string_table: String.Table,
 /// Symbol name of the entry function to export
-entry_name: ?[]const u8,
+entry_name: OptionalString,
 /// When true, will allow undefined symbols
 import_symbols: bool,
-/// List of *global* symbol names to export to the host environment.
+/// Set of *global* symbol names to export to the host environment.
 export_symbol_names: []const []const u8,
 /// When defined, sets the start of the data section.
 global_base: ?u64,
@@ -63,32 +68,14 @@ objects: std.ArrayListUnmanaged(Object) = .{},
 /// LLVM uses "env" by default when none is given. This would be a good default for Zig
 /// to support existing code.
 /// TODO: Allow setting this through a flag?
-host_name: []const u8 = "env",
+host_name: String,
 /// List of symbols generated by the linker.
 synthetic_symbols: std.ArrayListUnmanaged(Symbol) = .empty,
 /// Maps atoms to their segment index
-atoms: std.AutoHashMapUnmanaged(u32, Atom.Index) = .empty,
+atoms: std.AutoHashMapUnmanaged(Segment.Index, Atom.Index) = .empty,
 /// List of all atoms.
 managed_atoms: std.ArrayListUnmanaged(Atom) = .empty,
-/// Represents the index into `segments` where the 'code' section
-/// lives.
-code_section_index: ?u32 = null,
-/// The index of the segment representing the custom '.debug_info' section.
-debug_info_index: ?u32 = null,
-/// The index of the segment representing the custom '.debug_line' section.
-debug_line_index: ?u32 = null,
-/// The index of the segment representing the custom '.debug_loc' section.
-debug_loc_index: ?u32 = null,
-/// The index of the segment representing the custom '.debug_ranges' section.
-debug_ranges_index: ?u32 = null,
-/// The index of the segment representing the custom '.debug_pubnames' section.
-debug_pubnames_index: ?u32 = null,
-/// The index of the segment representing the custom '.debug_pubtypes' section.
-debug_pubtypes_index: ?u32 = null,
-/// The index of the segment representing the custom '.debug_pubtypes' section.
-debug_str_index: ?u32 = null,
-/// The index of the segment representing the custom '.debug_pubtypes' section.
-debug_abbrev_index: ?u32 = null,
+
 /// The count of imported functions. This number will be appended
 /// to the function indexes as their index starts at the lowest non-extern function.
 imported_functions_count: u32 = 0,
@@ -104,13 +91,11 @@ imports: std.AutoHashMapUnmanaged(SymbolLoc, Import) = .empty,
 /// Used for code, data and custom sections.
 segments: std.ArrayListUnmanaged(Segment) = .empty,
 /// Maps a data segment key (such as .rodata) to the index into `segments`.
-data_segments: std.StringArrayHashMapUnmanaged(u32) = .empty,
+data_segments: std.StringArrayHashMapUnmanaged(Segment.Index) = .empty,
 /// A table of `NamedSegment` which provide meta data
 /// about a data symbol such as its name where the key is
 /// the segment index, which can be found from `data_segments`
-segment_info: std.AutoArrayHashMapUnmanaged(u32, NamedSegment) = .empty,
-/// Deduplicated string table for strings used by symbols, imports and exports.
-string_table: StringTable = .{},
+segment_info: std.AutoArrayHashMapUnmanaged(Segment.Index, NamedSegment) = .empty,
 
 // Output sections
 /// Output type section
@@ -158,8 +143,8 @@ function_table: std.AutoHashMapUnmanaged(SymbolLoc, u32) = .empty,
 /// e.g. when an undefined symbol references a symbol from the archive.
 lazy_archives: std.ArrayListUnmanaged(LazyArchive) = .empty,
 
-/// A map of global names (read: offset into string table) to their symbol location
-globals: std.AutoHashMapUnmanaged(u32, SymbolLoc) = .empty,
+/// A map of global names to their symbol location
+globals: std.AutoArrayHashMapUnmanaged(String, SymbolLoc) = .empty,
 /// The list of GOT symbols and their location
 got_symbols: std.ArrayListUnmanaged(SymbolLoc) = .empty,
 /// Maps discarded symbols and their positions to the location of the symbol
@@ -169,8 +154,7 @@ discarded: std.AutoHashMapUnmanaged(SymbolLoc, SymbolLoc) = .empty,
 /// into the final binary.
 resolved_symbols: std.AutoArrayHashMapUnmanaged(SymbolLoc, void) = .empty,
 /// Symbols that remain undefined after symbol resolution.
-/// Note: The key represents an offset into the string table, rather than the actual string.
-undefs: std.AutoArrayHashMapUnmanaged(u32, SymbolLoc) = .empty,
+undefs: std.AutoArrayHashMapUnmanaged(String, SymbolLoc) = .empty,
 /// Maps a symbol's location to an atom. This can be used to find meta
 /// data of a symbol, such as its size, or its offset to perform a relocation.
 /// Undefined (and synthetic) symbols do not have an Atom and therefore cannot be mapped.
@@ -178,8 +162,103 @@ symbol_atom: std.AutoHashMapUnmanaged(SymbolLoc, Atom.Index) = .empty,
 
 /// `--verbose-link` output.
 /// Initialized on creation, appended to as inputs are added, printed during `flush`.
+/// String data is allocated into Compilation arena.
 dump_argv_list: std.ArrayListUnmanaged([]const u8),
 
+/// Represents the index into `segments` where the 'code' section lives.
+code_section_index: Segment.OptionalIndex = .none,
+custom_sections: CustomSections,
+preloaded_strings: PreloadedStrings,
+
+/// Type reflection is used on the field names to autopopulate each field
+/// during initialization.
+const PreloadedStrings = struct {
+    __heap_base: String,
+    __heap_end: String,
+    __indirect_function_table: String,
+    __linear_memory: String,
+    __stack_pointer: String,
+    __tls_align: String,
+    __tls_base: String,
+    __tls_size: String,
+    __wasm_apply_global_tls_relocs: String,
+    __wasm_call_ctors: String,
+    __wasm_init_memory: String,
+    __wasm_init_memory_flag: String,
+    __wasm_init_tls: String,
+    __zig_err_name_table: String,
+    __zig_err_names: String,
+    __zig_errors_len: String,
+    _initialize: String,
+    _start: String,
+    memory: String,
+};
+
+/// Type reflection is used on the field names to autopopulate each inner `name` field.
+const CustomSections = struct {
+    @".debug_info": CustomSection,
+    @".debug_pubtypes": CustomSection,
+    @".debug_abbrev": CustomSection,
+    @".debug_line": CustomSection,
+    @".debug_str": CustomSection,
+    @".debug_pubnames": CustomSection,
+    @".debug_loc": CustomSection,
+    @".debug_ranges": CustomSection,
+};
+
+const CustomSection = struct {
+    name: String,
+    index: Segment.OptionalIndex,
+};
+
+/// Index into string_bytes
+pub const String = enum(u32) {
+    _,
+
+    const Table = std.HashMapUnmanaged(String, void, TableContext, std.hash_map.default_max_load_percentage);
+
+    const TableContext = struct {
+        bytes: []const u8,
+
+        pub fn eql(_: @This(), a: String, b: String) bool {
+            return a == b;
+        }
+
+        pub fn hash(ctx: @This(), key: String) u64 {
+            return std.hash_map.hashString(mem.sliceTo(ctx.bytes[@intFromEnum(key)..], 0));
+        }
+    };
+
+    const TableIndexAdapter = struct {
+        bytes: []const u8,
+
+        pub fn eql(ctx: @This(), a: []const u8, b: String) bool {
+            return mem.eql(u8, a, mem.sliceTo(ctx.bytes[@intFromEnum(b)..], 0));
+        }
+
+        pub fn hash(_: @This(), adapted_key: []const u8) u64 {
+            assert(mem.indexOfScalar(u8, adapted_key, 0) == null);
+            return std.hash_map.hashString(adapted_key);
+        }
+    };
+
+    pub fn toOptional(i: String) OptionalString {
+        const result: OptionalString = @enumFromInt(@intFromEnum(i));
+        assert(result != .none);
+        return result;
+    }
+};
+
+pub const OptionalString = enum(u32) {
+    none = std.math.maxInt(u32),
+    _,
+
+    pub fn unwrap(i: OptionalString) ?String {
+        if (i == .none) return null;
+        return @enumFromInt(@intFromEnum(i));
+    }
+};
+
 /// Index into objects array or the zig object.
 pub const ObjectId = enum(u16) {
     zig_object = std.math.maxInt(u16) - 1,
@@ -222,6 +301,26 @@ pub const Segment = struct {
     offset: u32,
     flags: u32,
 
+    const Index = enum(u32) {
+        _,
+
+        pub fn toOptional(i: Index) OptionalIndex {
+            const result: OptionalIndex = @enumFromInt(@intFromEnum(i));
+            assert(result != .none);
+            return result;
+        }
+    };
+
+    const OptionalIndex = enum(u32) {
+        none = std.math.maxInt(u32),
+        _,
+
+        pub fn unwrap(i: OptionalIndex) ?Index {
+            if (i == .none) return null;
+            return @enumFromInt(@intFromEnum(i));
+        }
+    };
+
     pub const Flag = enum(u32) {
         WASM_DATA_SEGMENT_IS_PASSIVE = 0x01,
         WASM_DATA_SEGMENT_HAS_MEMINDEX = 0x02,
@@ -260,26 +359,8 @@ pub fn symbolLocSymbol(wasm: *const Wasm, loc: SymbolLoc) *Symbol {
 }
 
 /// From a given location, returns the name of the symbol.
-pub fn symbolLocName(wasm: *const Wasm, loc: SymbolLoc) []const u8 {
-    if (wasm.discarded.get(loc)) |new_loc| {
-        return wasm.symbolLocName(new_loc);
-    }
-    switch (loc.file) {
-        .none => {
-            const sym = wasm.synthetic_symbols.items[@intFromEnum(loc.index)];
-            return wasm.string_table.get(sym.name);
-        },
-        .zig_object => {
-            const zo = wasm.zig_object.?;
-            const sym = zo.symbols.items[@intFromEnum(loc.index)];
-            return zo.string_table.get(sym.name).?;
-        },
-        _ => {
-            const obj = &wasm.objects.items[@intFromEnum(loc.file)];
-            const sym = obj.symtable[@intFromEnum(loc.index)];
-            return obj.string_table.get(sym.name);
-        },
-    }
+pub fn symbolLocName(wasm: *const Wasm, loc: SymbolLoc) [:0]const u8 {
+    return wasm.stringSlice(wasm.symbolLocSymbol(loc).name);
 }
 
 /// From a given symbol location, returns the final location.
@@ -325,75 +406,6 @@ pub const InitFuncLoc = struct {
         return lhs.priority < rhs.priority;
     }
 };
-/// Generic string table that duplicates strings
-/// and converts them into offsets instead.
-pub const StringTable = struct {
-    /// Table that maps string offsets, which is used to de-duplicate strings.
-    /// Rather than having the offset map to the data, the `StringContext` holds all bytes of the string.
-    /// The strings are stored as a contigious array where each string is zero-terminated.
-    string_table: std.HashMapUnmanaged(
-        u32,
-        void,
-        std.hash_map.StringIndexContext,
-        std.hash_map.default_max_load_percentage,
-    ) = .{},
-    /// Holds the actual data of the string table.
-    string_data: std.ArrayListUnmanaged(u8) = .empty,
-
-    /// Accepts a string and searches for a corresponding string.
-    /// When found, de-duplicates the string and returns the existing offset instead.
-    /// When the string is not found in the `string_table`, a new entry will be inserted
-    /// and the new offset to its data will be returned.
-    pub fn put(table: *StringTable, allocator: Allocator, string: []const u8) !u32 {
-        const gop = try table.string_table.getOrPutContextAdapted(
-            allocator,
-            string,
-            std.hash_map.StringIndexAdapter{ .bytes = &table.string_data },
-            .{ .bytes = &table.string_data },
-        );
-        if (gop.found_existing) {
-            const off = gop.key_ptr.*;
-            log.debug("reusing string '{s}' at offset 0x{x}", .{ string, off });
-            return off;
-        }
-
-        try table.string_data.ensureUnusedCapacity(allocator, string.len + 1);
-        const offset: u32 = @intCast(table.string_data.items.len);
-
-        log.debug("writing new string '{s}' at offset 0x{x}", .{ string, offset });
-
-        table.string_data.appendSliceAssumeCapacity(string);
-        table.string_data.appendAssumeCapacity(0);
-
-        gop.key_ptr.* = offset;
-
-        return offset;
-    }
-
-    /// From a given offset, returns its corresponding string value.
-    /// Asserts offset does not exceed bounds.
-    pub fn get(table: StringTable, off: u32) []const u8 {
-        assert(off < table.string_data.items.len);
-        return mem.sliceTo(@as([*:0]const u8, @ptrCast(table.string_data.items.ptr + off)), 0);
-    }
-
-    /// Returns the offset of a given string when it exists.
-    /// Will return null if the given string does not yet exist within the string table.
-    pub fn getOffset(table: *StringTable, string: []const u8) ?u32 {
-        return table.string_table.getKeyAdapted(
-            string,
-            std.hash_map.StringIndexAdapter{ .bytes = &table.string_data },
-        );
-    }
-
-    /// Frees all resources of the string table. Any references pointing
-    /// to the strings will be invalid.
-    pub fn deinit(table: *StringTable, allocator: Allocator) void {
-        table.string_data.deinit(allocator);
-        table.string_table.deinit(allocator);
-        table.* = undefined;
-    }
-};
 
 pub fn open(
     arena: Allocator,
@@ -451,6 +463,8 @@ pub fn createEmpty(
             .build_id = options.build_id,
         },
         .name = undefined,
+        .string_table = .empty,
+        .string_bytes = .empty,
         .import_table = options.import_table,
         .export_table = options.export_table,
         .import_symbols = options.import_symbols,
@@ -459,20 +473,38 @@ pub fn createEmpty(
         .initial_memory = options.initial_memory,
         .max_memory = options.max_memory,
 
-        .entry_name = switch (options.entry) {
-            .disabled => null,
-            .default => if (output_mode != .Exe) null else defaultEntrySymbolName(wasi_exec_model),
-            .enabled => defaultEntrySymbolName(wasi_exec_model),
-            .named => |name| name,
-        },
+        .entry_name = undefined,
         .zig_object = null,
         .dump_argv_list = .empty,
+        .host_name = undefined,
+        .custom_sections = undefined,
+        .preloaded_strings = undefined,
     };
     if (use_llvm and comp.config.have_zcu) {
         wasm.llvm_object = try LlvmObject.create(arena, comp);
     }
     errdefer wasm.base.destroy();
 
+    wasm.host_name = try wasm.internString("env");
+
+    inline for (@typeInfo(CustomSections).@"struct".fields) |field| {
+        @field(wasm.custom_sections, field.name) = .{
+            .index = .none,
+            .name = try wasm.internString(field.name),
+        };
+    }
+
+    inline for (@typeInfo(PreloadedStrings).@"struct".fields) |field| {
+        @field(wasm.preloaded_strings, field.name) = try wasm.internString(field.name);
+    }
+
+    wasm.entry_name = switch (options.entry) {
+        .disabled => .none,
+        .default => if (output_mode != .Exe) .none else defaultEntrySymbolName(&wasm.preloaded_strings, wasi_exec_model).toOptional(),
+        .enabled => defaultEntrySymbolName(&wasm.preloaded_strings, wasi_exec_model).toOptional(),
+        .named => |name| (try wasm.internString(name)).toOptional(),
+    };
+
     if (use_lld and (use_llvm or !comp.config.have_zcu)) {
         // LLVM emits the object file (if any); LLD links it into the final product.
         return wasm;
@@ -498,22 +530,18 @@ pub fn createEmpty(
 
     // create stack pointer symbol
     {
-        const loc = try wasm.createSyntheticSymbol("__stack_pointer", .global);
+        const loc = try wasm.createSyntheticSymbol(wasm.preloaded_strings.__stack_pointer, .global);
         const symbol = wasm.symbolLocSymbol(loc);
         // For object files we will import the stack pointer symbol
         if (output_mode == .Obj) {
             symbol.setUndefined(true);
             symbol.index = @intCast(wasm.imported_globals_count);
             wasm.imported_globals_count += 1;
-            try wasm.imports.putNoClobber(
-                gpa,
-                loc,
-                .{
-                    .module_name = try wasm.string_table.put(gpa, wasm.host_name),
-                    .name = symbol.name,
-                    .kind = .{ .global = .{ .valtype = .i32, .mutable = true } },
-                },
-            );
+            try wasm.imports.putNoClobber(gpa, loc, .{
+                .module_name = wasm.host_name,
+                .name = symbol.name,
+                .kind = .{ .global = .{ .valtype = .i32, .mutable = true } },
+            });
         } else {
             symbol.index = @intCast(wasm.imported_globals_count + wasm.wasm_globals.items.len);
             symbol.setFlag(.WASM_SYM_VISIBILITY_HIDDEN);
@@ -530,7 +558,7 @@ pub fn createEmpty(
 
     // create indirect function pointer symbol
     {
-        const loc = try wasm.createSyntheticSymbol("__indirect_function_table", .table);
+        const loc = try wasm.createSyntheticSymbol(wasm.preloaded_strings.__indirect_function_table, .table);
         const symbol = wasm.symbolLocSymbol(loc);
         const table: std.wasm.Table = .{
             .limits = .{ .flags = 0, .min = 0, .max = undefined }, // will be overwritten during `mapFunctionTable`
@@ -541,7 +569,7 @@ pub fn createEmpty(
             symbol.index = @intCast(wasm.imported_tables_count);
             wasm.imported_tables_count += 1;
             try wasm.imports.put(gpa, loc, .{
-                .module_name = try wasm.string_table.put(gpa, wasm.host_name),
+                .module_name = wasm.host_name,
                 .name = symbol.name,
                 .kind = .{ .table = table },
             });
@@ -558,7 +586,7 @@ pub fn createEmpty(
 
     // create __wasm_call_ctors
     {
-        const loc = try wasm.createSyntheticSymbol("__wasm_call_ctors", .function);
+        const loc = try wasm.createSyntheticSymbol(wasm.preloaded_strings.__wasm_call_ctors, .function);
         const symbol = wasm.symbolLocSymbol(loc);
         symbol.setFlag(.WASM_SYM_VISIBILITY_HIDDEN);
         // we do not know the function index until after we merged all sections.
@@ -569,7 +597,7 @@ pub fn createEmpty(
     // shared-memory symbols for TLS support
     if (shared_memory) {
         {
-            const loc = try wasm.createSyntheticSymbol("__tls_base", .global);
+            const loc = try wasm.createSyntheticSymbol(wasm.preloaded_strings.__tls_base, .global);
             const symbol = wasm.symbolLocSymbol(loc);
             symbol.setFlag(.WASM_SYM_VISIBILITY_HIDDEN);
             symbol.index = @intCast(wasm.imported_globals_count + wasm.wasm_globals.items.len);
@@ -580,7 +608,7 @@ pub fn createEmpty(
             });
         }
         {
-            const loc = try wasm.createSyntheticSymbol("__tls_size", .global);
+            const loc = try wasm.createSyntheticSymbol(wasm.preloaded_strings.__tls_size, .global);
             const symbol = wasm.symbolLocSymbol(loc);
             symbol.setFlag(.WASM_SYM_VISIBILITY_HIDDEN);
             symbol.index = @intCast(wasm.imported_globals_count + wasm.wasm_globals.items.len);
@@ -591,7 +619,7 @@ pub fn createEmpty(
             });
         }
         {
-            const loc = try wasm.createSyntheticSymbol("__tls_align", .global);
+            const loc = try wasm.createSyntheticSymbol(wasm.preloaded_strings.__tls_align, .global);
             const symbol = wasm.symbolLocSymbol(loc);
             symbol.setFlag(.WASM_SYM_VISIBILITY_HIDDEN);
             symbol.index = @intCast(wasm.imported_globals_count + wasm.wasm_globals.items.len);
@@ -602,7 +630,7 @@ pub fn createEmpty(
             });
         }
         {
-            const loc = try wasm.createSyntheticSymbol("__wasm_init_tls", .function);
+            const loc = try wasm.createSyntheticSymbol(wasm.preloaded_strings.__wasm_init_tls, .function);
             const symbol = wasm.symbolLocSymbol(loc);
             symbol.setFlag(.WASM_SYM_VISIBILITY_HIDDEN);
         }
@@ -655,13 +683,11 @@ pub fn addOrUpdateImport(
 
 /// 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 gpa = wasm.base.comp.gpa;
-    const name_offset = try wasm.string_table.put(gpa, name);
-    return wasm.createSyntheticSymbolOffset(name_offset, tag);
+fn createSyntheticSymbol(wasm: *Wasm, name: String, tag: Symbol.Tag) !SymbolLoc {
+    return wasm.createSyntheticSymbolOffset(name, tag);
 }
 
-fn createSyntheticSymbolOffset(wasm: *Wasm, name_offset: u32, tag: Symbol.Tag) !SymbolLoc {
+fn createSyntheticSymbolOffset(wasm: *Wasm, name_offset: String, tag: Symbol.Tag) !SymbolLoc {
     const sym_index: Symbol.Index = @enumFromInt(wasm.synthetic_symbols.items.len);
     const loc: SymbolLoc = .{ .index = sym_index, .file = .none };
     const gpa = wasm.base.comp.gpa;
@@ -803,16 +829,6 @@ fn objectSymbol(wasm: *const Wasm, object_id: ObjectId, index: Symbol.Index) *Sy
     return &obj.symtable[@intFromEnum(index)];
 }
 
-fn objectSymbolName(wasm: *const Wasm, object_id: ObjectId, index: Symbol.Index) []const u8 {
-    const obj = wasm.objectById(object_id) orelse {
-        const zo = wasm.zig_object.?;
-        const sym = zo.symbols.items[@intFromEnum(index)];
-        return zo.string_table.get(sym.name).?;
-    };
-    const sym = obj.symtable[@intFromEnum(index)];
-    return obj.string_table.get(sym.name);
-}
-
 fn objectFunction(wasm: *const Wasm, object_id: ObjectId, sym_index: Symbol.Index) std.wasm.Func {
     const obj = wasm.objectById(object_id) orelse {
         const zo = wasm.zig_object.?;
@@ -850,13 +866,6 @@ fn objectImport(wasm: *const Wasm, object_id: ObjectId, symbol_index: Symbol.Ind
     return obj.findImport(obj.symtable[@intFromEnum(symbol_index)]);
 }
 
-/// For a given offset, returns its string value.
-/// Asserts string exists in the object string table.
-fn objectString(wasm: *const Wasm, object_id: ObjectId, offset: u32) []const u8 {
-    const obj = wasm.objectById(object_id) orelse return wasm.zig_object.?.string_table.get(offset).?;
-    return obj.string_table.get(offset);
-}
-
 /// Returns the object element pointer, or null if it is the ZigObject.
 fn objectById(wasm: *const Wasm, object_id: ObjectId) ?*Object {
     if (object_id == .zig_object) return null;
@@ -876,27 +885,25 @@ fn resolveSymbolsInObject(wasm: *Wasm, object_id: ObjectId) !void {
             .file = object_id.toOptional(),
             .index = sym_index,
         };
-        const sym_name = objectString(wasm, object_id, symbol.name);
-        if (mem.eql(u8, sym_name, "__indirect_function_table")) {
-            continue;
-        }
-        const sym_name_index = try wasm.string_table.put(gpa, sym_name);
+        if (symbol.name == wasm.preloaded_strings.__indirect_function_table) continue;
 
         if (symbol.isLocal()) {
             if (symbol.isUndefined()) {
-                diags.addParseError(obj_path, "local symbol '{s}' references import", .{sym_name});
+                diags.addParseError(obj_path, "local symbol '{s}' references import", .{
+                    wasm.stringSlice(symbol.name),
+                });
             }
             try wasm.resolved_symbols.putNoClobber(gpa, location, {});
             continue;
         }
 
-        const maybe_existing = try wasm.globals.getOrPut(gpa, sym_name_index);
+        const maybe_existing = try wasm.globals.getOrPut(gpa, symbol.name);
         if (!maybe_existing.found_existing) {
             maybe_existing.value_ptr.* = location;
             try wasm.resolved_symbols.putNoClobber(gpa, location, {});
 
             if (symbol.isUndefined()) {
-                try wasm.undefs.putNoClobber(gpa, sym_name_index, location);
+                try wasm.undefs.putNoClobber(gpa, symbol.name, location);
             }
             continue;
         }
@@ -918,7 +925,7 @@ fn resolveSymbolsInObject(wasm: *Wasm, object_id: ObjectId) !void {
                 }
                 // both are defined and weak, we have a symbol collision.
                 var err = try diags.addErrorWithNotes(2);
-                try err.addMsg("symbol '{s}' defined multiple times", .{sym_name});
+                try err.addMsg("symbol '{s}' defined multiple times", .{wasm.stringSlice(symbol.name)});
                 try err.addNote("first definition in '{'}'", .{existing_file_path});
                 try err.addNote("next definition in '{'}'", .{obj_path});
             }
@@ -929,7 +936,9 @@ fn resolveSymbolsInObject(wasm: *Wasm, object_id: ObjectId) !void {
 
         if (symbol.tag != existing_sym.tag) {
             var err = try diags.addErrorWithNotes(2);
-            try err.addMsg("symbol '{s}' mismatching types '{s}' and '{s}'", .{ sym_name, @tagName(symbol.tag), @tagName(existing_sym.tag) });
+            try err.addMsg("symbol '{s}' mismatching types '{s}' and '{s}'", .{
+                wasm.stringSlice(symbol.name), @tagName(symbol.tag), @tagName(existing_sym.tag),
+            });
             try err.addNote("first definition in '{'}'", .{existing_file_path});
             try err.addNote("next definition in '{'}'", .{obj_path});
         }
@@ -937,22 +946,18 @@ fn resolveSymbolsInObject(wasm: *Wasm, object_id: ObjectId) !void {
         if (existing_sym.isUndefined() and symbol.isUndefined()) {
             // only verify module/import name for function symbols
             if (symbol.tag == .function) {
-                const existing_name = if (existing_loc.file.unwrap()) |existing_obj_id| blk: {
-                    const imp = objectImport(wasm, existing_obj_id, existing_loc.index);
-                    break :blk objectString(wasm, existing_obj_id, imp.module_name);
-                } else blk: {
-                    const name_index = wasm.imports.get(existing_loc).?.module_name;
-                    break :blk wasm.string_table.get(name_index);
-                };
+                const existing_name = if (existing_loc.file.unwrap()) |existing_obj_id|
+                    objectImport(wasm, existing_obj_id, existing_loc.index).module_name
+                else
+                    wasm.imports.get(existing_loc).?.module_name;
 
-                const imp = objectImport(wasm, object_id, sym_index);
-                const module_name = objectString(wasm, object_id, imp.module_name);
-                if (!mem.eql(u8, existing_name, module_name)) {
+                const module_name = objectImport(wasm, object_id, sym_index).module_name;
+                if (existing_name != module_name) {
                     var err = try diags.addErrorWithNotes(2);
                     try err.addMsg("symbol '{s}' module name mismatch. Expected '{s}', but found '{s}'", .{
-                        sym_name,
-                        existing_name,
-                        module_name,
+                        wasm.stringSlice(symbol.name),
+                        wasm.stringSlice(existing_name),
+                        wasm.stringSlice(module_name),
                     });
                     try err.addNote("first definition in '{'}'", .{existing_file_path});
                     try err.addNote("next definition in '{'}'", .{obj_path});
@@ -969,7 +974,7 @@ fn resolveSymbolsInObject(wasm: *Wasm, object_id: ObjectId) !void {
             const new_ty = wasm.getGlobalType(location);
             if (existing_ty.mutable != new_ty.mutable or existing_ty.valtype != new_ty.valtype) {
                 var err = try diags.addErrorWithNotes(2);
-                try err.addMsg("symbol '{s}' mismatching global types", .{sym_name});
+                try err.addMsg("symbol '{s}' mismatching global types", .{wasm.stringSlice(symbol.name)});
                 try err.addNote("first definition in '{'}'", .{existing_file_path});
                 try err.addNote("next definition in '{'}'", .{obj_path});
             }
@@ -980,7 +985,7 @@ fn resolveSymbolsInObject(wasm: *Wasm, object_id: ObjectId) !void {
             const new_ty = wasm.getFunctionSignature(location);
             if (!existing_ty.eql(new_ty)) {
                 var err = try diags.addErrorWithNotes(3);
-                try err.addMsg("symbol '{s}' mismatching function signatures.", .{sym_name});
+                try err.addMsg("symbol '{s}' mismatching function signatures.", .{wasm.stringSlice(symbol.name)});
                 try err.addNote("expected signature {}, but found signature {}", .{ existing_ty, new_ty });
                 try err.addNote("first definition in '{'}'", .{existing_file_path});
                 try err.addNote("next definition in '{'}'", .{obj_path});
@@ -996,16 +1001,16 @@ fn resolveSymbolsInObject(wasm: *Wasm, object_id: ObjectId) !void {
         }
 
         // simply overwrite with the new symbol
-        log.debug("Overwriting symbol '{s}'", .{sym_name});
+        log.debug("Overwriting symbol '{s}'", .{wasm.stringSlice(symbol.name)});
         log.debug("  old definition in '{'}'", .{existing_file_path});
         log.debug("  new definition in '{'}'", .{obj_path});
         try wasm.discarded.putNoClobber(gpa, existing_loc, location);
         maybe_existing.value_ptr.* = location;
-        try wasm.globals.put(gpa, sym_name_index, location);
+        try wasm.globals.put(gpa, symbol.name, location);
         try wasm.resolved_symbols.put(gpa, location, {});
         assert(wasm.resolved_symbols.swapRemove(existing_loc));
         if (existing_sym.isUndefined()) {
-            _ = wasm.undefs.swapRemove(sym_name_index);
+            _ = wasm.undefs.swapRemove(symbol.name);
         }
     }
 }
@@ -1021,7 +1026,7 @@ fn resolveSymbolsInArchives(wasm: *Wasm) !void {
         const sym_name_index = wasm.undefs.keys()[index];
 
         for (wasm.lazy_archives.items) |lazy_archive| {
-            const sym_name = wasm.string_table.get(sym_name_index);
+            const sym_name = wasm.stringSlice(sym_name_index);
             log.debug("Detected symbol '{s}' in archive '{'}', parsing objects..", .{
                 sym_name, lazy_archive.path,
             });
@@ -1066,13 +1071,13 @@ fn setupInitMemoryFunction(wasm: *Wasm) !void {
     if (!wasm.hasPassiveInitializationSegments()) {
         return;
     }
-    const sym_loc = try wasm.createSyntheticSymbol("__wasm_init_memory", .function);
+    const sym_loc = try wasm.createSyntheticSymbol(wasm.preloaded_strings.__wasm_init_memory, .function);
     wasm.symbolLocSymbol(sym_loc).mark();
 
     const flag_address: u32 = if (shared_memory) address: {
         // when we have passive initialization segments and shared memory
         // `setupMemory` will create this symbol and set its virtual address.
-        const loc = wasm.findGlobalSymbol("__wasm_init_memory_flag").?;
+        const loc = wasm.globals.get(wasm.preloaded_strings.__wasm_init_memory_flag).?;
         break :address wasm.symbolLocSymbol(loc).virtual_address;
     } else 0;
 
@@ -1113,31 +1118,30 @@ fn setupInitMemoryFunction(wasm: *Wasm) !void {
         try writer.writeByte(std.wasm.opcode(.end));
     }
 
-    var it = wasm.data_segments.iterator();
-    var segment_index: u32 = 0;
-    while (it.next()) |entry| : (segment_index += 1) {
-        const segment: Segment = wasm.segments.items[entry.value_ptr.*];
-        if (segment.needsPassiveInitialization(import_memory, entry.key_ptr.*)) {
+    for (wasm.data_segments.keys(), wasm.data_segments.values(), 0..) |key, value, segment_index_usize| {
+        const segment_index: u32 = @intCast(segment_index_usize);
+        const segment = wasm.segmentPtr(value);
+        if (segment.needsPassiveInitialization(import_memory, key)) {
             // For passive BSS segments we can simple issue a memory.fill(0).
             // For non-BSS segments we do a memory.init.  Both these
             // instructions take as their first argument the destination
             // address.
             try writeI32Const(writer, segment.offset);
 
-            if (shared_memory and std.mem.eql(u8, entry.key_ptr.*, ".tdata")) {
+            if (shared_memory and std.mem.eql(u8, key, ".tdata")) {
                 // When we initialize the TLS segment we also set the `__tls_base`
                 // global.  This allows the runtime to use this static copy of the
                 // TLS data for the first/main thread.
                 try writeI32Const(writer, segment.offset);
                 try writer.writeByte(std.wasm.opcode(.global_set));
-                const loc = wasm.findGlobalSymbol("__tls_base").?;
+                const loc = wasm.globals.get(wasm.preloaded_strings.__tls_base).?;
                 try leb.writeUleb128(writer, wasm.symbolLocSymbol(loc).index);
             }
 
             try writeI32Const(writer, 0);
             try writeI32Const(writer, segment.size);
             try writer.writeByte(std.wasm.opcode(.misc_prefix));
-            if (std.mem.eql(u8, entry.key_ptr.*, ".bss")) {
+            if (std.mem.eql(u8, key, ".bss")) {
                 // fill bss segment with zeroes
                 try leb.writeUleb128(writer, std.wasm.miscOpcode(.memory_fill));
             } else {
@@ -1187,11 +1191,9 @@ fn setupInitMemoryFunction(wasm: *Wasm) !void {
         try writer.writeByte(std.wasm.opcode(.end)); // end $drop
     }
 
-    it.reset();
-    segment_index = 0;
-    while (it.next()) |entry| : (segment_index += 1) {
-        const name = entry.key_ptr.*;
-        const segment: Segment = wasm.segments.items[entry.value_ptr.*];
+    for (wasm.data_segments.keys(), wasm.data_segments.values(), 0..) |name, value, segment_index_usize| {
+        const segment_index: u32 = @intCast(segment_index_usize);
+        const segment = wasm.segmentPtr(value);
         if (segment.needsPassiveInitialization(import_memory, name) and
             !std.mem.eql(u8, name, ".bss"))
         {
@@ -1211,7 +1213,7 @@ fn setupInitMemoryFunction(wasm: *Wasm) !void {
     try writer.writeByte(std.wasm.opcode(.end));
 
     try wasm.createSyntheticFunction(
-        "__wasm_init_memory",
+        wasm.preloaded_strings.__wasm_init_memory,
         std.wasm.Type{ .params = &.{}, .returns = &.{} },
         &function_body,
     );
@@ -1230,7 +1232,7 @@ fn setupTLSRelocationsFunction(wasm: *Wasm) !void {
         return;
     }
 
-    const loc = try wasm.createSyntheticSymbol("__wasm_apply_global_tls_relocs", .function);
+    const loc = try wasm.createSyntheticSymbol(wasm.preloaded_strings.__wasm_apply_global_tls_relocs, .function);
     wasm.symbolLocSymbol(loc).mark();
     var function_body = std.ArrayList(u8).init(gpa);
     defer function_body.deinit();
@@ -1244,7 +1246,7 @@ fn setupTLSRelocationsFunction(wasm: *Wasm) !void {
         if (sym.tag == .data and sym.isDefined()) {
             // get __tls_base
             try writer.writeByte(std.wasm.opcode(.global_get));
-            try leb.writeUleb128(writer, wasm.symbolLocSymbol(wasm.findGlobalSymbol("__tls_base").?).index);
+            try leb.writeUleb128(writer, wasm.symbolLocSymbol(wasm.globals.get(wasm.preloaded_strings.__tls_base).?).index);
 
             // add the virtual address of the symbol
             try writer.writeByte(std.wasm.opcode(.i32_const));
@@ -1260,7 +1262,7 @@ fn setupTLSRelocationsFunction(wasm: *Wasm) !void {
     try writer.writeByte(std.wasm.opcode(.end));
 
     try wasm.createSyntheticFunction(
-        "__wasm_apply_global_tls_relocs",
+        wasm.preloaded_strings.__wasm_apply_global_tls_relocs,
         std.wasm.Type{ .params = &.{}, .returns = &.{} },
         &function_body,
     );
@@ -1422,7 +1424,7 @@ fn resolveLazySymbols(wasm: *Wasm) !void {
     const gpa = comp.gpa;
     const shared_memory = comp.config.shared_memory;
 
-    if (wasm.string_table.getOffset("__heap_base")) |name_offset| {
+    if (wasm.getExistingString("__heap_base")) |name_offset| {
         if (wasm.undefs.fetchSwapRemove(name_offset)) |kv| {
             const loc = try wasm.createSyntheticSymbolOffset(name_offset, .data);
             try wasm.discarded.putNoClobber(gpa, kv.value, loc);
@@ -1430,7 +1432,7 @@ fn resolveLazySymbols(wasm: *Wasm) !void {
         }
     }
 
-    if (wasm.string_table.getOffset("__heap_end")) |name_offset| {
+    if (wasm.getExistingString("__heap_end")) |name_offset| {
         if (wasm.undefs.fetchSwapRemove(name_offset)) |kv| {
             const loc = try wasm.createSyntheticSymbolOffset(name_offset, .data);
             try wasm.discarded.putNoClobber(gpa, kv.value, loc);
@@ -1439,7 +1441,7 @@ fn resolveLazySymbols(wasm: *Wasm) !void {
     }
 
     if (!shared_memory) {
-        if (wasm.string_table.getOffset("__tls_base")) |name_offset| {
+        if (wasm.getExistingString("__tls_base")) |name_offset| {
             if (wasm.undefs.fetchSwapRemove(name_offset)) |kv| {
                 const loc = try wasm.createSyntheticSymbolOffset(name_offset, .global);
                 try wasm.discarded.putNoClobber(gpa, kv.value, loc);
@@ -1456,11 +1458,9 @@ fn resolveLazySymbols(wasm: *Wasm) !void {
     }
 }
 
-// Tries to find a global symbol by its name. Returns null when not found,
-/// and its location when it is found.
-pub fn findGlobalSymbol(wasm: *Wasm, name: []const u8) ?SymbolLoc {
-    const offset = wasm.string_table.getOffset(name) orelse return null;
-    return wasm.globals.get(offset);
+pub fn findGlobalSymbol(wasm: *const Wasm, name: []const u8) ?SymbolLoc {
+    const name_index = wasm.getExistingString(name) orelse return null;
+    return wasm.globals.get(name_index);
 }
 
 fn checkUndefinedSymbols(wasm: *const Wasm) !void {
@@ -1516,7 +1516,7 @@ pub fn deinit(wasm: *Wasm) void {
     for (wasm.lazy_archives.items) |*lazy_archive| lazy_archive.deinit(gpa);
     wasm.lazy_archives.deinit(gpa);
 
-    if (wasm.findGlobalSymbol("__wasm_init_tls")) |loc| {
+    if (wasm.globals.get(wasm.preloaded_strings.__wasm_init_tls)) |loc| {
         const atom = wasm.symbol_atom.get(loc).?;
         wasm.getAtomPtr(atom).deinit(gpa);
     }
@@ -1544,6 +1544,7 @@ pub fn deinit(wasm: *Wasm) void {
     wasm.init_funcs.deinit(gpa);
     wasm.exports.deinit(gpa);
 
+    wasm.string_bytes.deinit(gpa);
     wasm.string_table.deinit(gpa);
     wasm.dump_argv_list.deinit(gpa);
 }
@@ -1649,7 +1650,8 @@ fn getFunctionSignature(wasm: *const Wasm, loc: SymbolLoc) std.wasm.Type {
 /// and then returns the index to it.
 pub fn getGlobalSymbol(wasm: *Wasm, name: []const u8, lib_name: ?[]const u8) !Symbol.Index {
     _ = lib_name;
-    return wasm.zig_object.?.getGlobalSymbol(wasm.base.comp.gpa, name);
+    const name_index = try wasm.internString(name);
+    return wasm.zig_object.?.getGlobalSymbol(wasm.base.comp.gpa, name_index);
 }
 
 /// For a given `Nav`, find the given symbol index's atom, and create a relocation for the type.
@@ -1721,12 +1723,12 @@ fn mapFunctionTable(wasm: *Wasm) void {
     }
 
     if (wasm.import_table or wasm.base.comp.config.output_mode == .Obj) {
-        const sym_loc = wasm.findGlobalSymbol("__indirect_function_table").?;
+        const sym_loc = wasm.globals.get(wasm.preloaded_strings.__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 sym_loc = wasm.findGlobalSymbol("__indirect_function_table").?;
+        const sym_loc = wasm.globals.get(wasm.preloaded_strings.__indirect_function_table).?;
         const symbol = wasm.symbolLocSymbol(sym_loc);
         const table = &wasm.tables.items[symbol.index - wasm.imported_tables_count];
         table.limits = .{ .min = index, .max = index, .flags = 0x1 };
@@ -1735,7 +1737,7 @@ fn mapFunctionTable(wasm: *Wasm) void {
 
 /// From a given index, append the given `Atom` at the back of the linked list.
 /// Simply inserts it into the map of atoms when it doesn't exist yet.
-pub fn appendAtomAtIndex(wasm: *Wasm, index: u32, atom_index: Atom.Index) !void {
+pub fn appendAtomAtIndex(wasm: *Wasm, index: Segment.Index, atom_index: Atom.Index) !void {
     const gpa = wasm.base.comp.gpa;
     const atom = wasm.getAtomPtr(atom_index);
     if (wasm.atoms.getPtr(index)) |last_index_ptr| {
@@ -1752,9 +1754,9 @@ fn allocateAtoms(wasm: *Wasm) !void {
 
     var it = wasm.atoms.iterator();
     while (it.next()) |entry| {
-        const segment = &wasm.segments.items[entry.key_ptr.*];
+        const segment = wasm.segmentPtr(entry.key_ptr.*);
         var atom_index = entry.value_ptr.*;
-        if (entry.key_ptr.* == wasm.code_section_index) {
+        if (entry.key_ptr.toOptional() == wasm.code_section_index) {
             // Code section is allocated upon writing as they are required to be ordered
             // to synchronise with the function section.
             continue;
@@ -1825,7 +1827,7 @@ fn allocateVirtualAddresses(wasm: *Wasm) void {
         };
         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];
+        const segment = wasm.segmentPtr(segment_index);
 
         // TLS symbols have their virtual address set relative to their own TLS segment,
         // rather than the entire Data section.
@@ -1839,7 +1841,7 @@ fn allocateVirtualAddresses(wasm: *Wasm) void {
 
 fn sortDataSegments(wasm: *Wasm) !void {
     const gpa = wasm.base.comp.gpa;
-    var new_mapping: std.StringArrayHashMapUnmanaged(u32) = .empty;
+    var new_mapping: std.StringArrayHashMapUnmanaged(Segment.Index) = .empty;
     try new_mapping.ensureUnusedCapacity(gpa, wasm.data_segments.count());
     errdefer new_mapping.deinit(gpa);
 
@@ -1894,9 +1896,9 @@ fn setupInitFunctions(wasm: *Wasm) !void {
             };
             if (ty.params.len != 0) {
                 var err = try diags.addErrorWithNotes(0);
-                try err.addMsg("constructor functions cannot take arguments: '{s}'", .{object.string_table.get(symbol.name)});
+                try err.addMsg("constructor functions cannot take arguments: '{s}'", .{wasm.stringSlice(symbol.name)});
             }
-            log.debug("appended init func '{s}'\n", .{object.string_table.get(symbol.name)});
+            log.debug("appended init func '{s}'\n", .{wasm.stringSlice(symbol.name)});
             wasm.init_funcs.appendAssumeCapacity(.{
                 .index = @enumFromInt(init_func.symbol_index),
                 .file = @enumFromInt(object_index),
@@ -1913,7 +1915,7 @@ fn setupInitFunctions(wasm: *Wasm) !void {
     mem.sort(InitFuncLoc, wasm.init_funcs.items, {}, InitFuncLoc.lessThan);
 
     if (wasm.init_funcs.items.len > 0) {
-        const loc = wasm.findGlobalSymbol("__wasm_call_ctors").?;
+        const loc = wasm.globals.get(wasm.preloaded_strings.__wasm_call_ctors).?;
         try wasm.mark(loc);
     }
 }
@@ -1927,10 +1929,10 @@ fn setupInitFunctions(wasm: *Wasm) !void {
 fn initializeCallCtorsFunction(wasm: *Wasm) !void {
     const gpa = wasm.base.comp.gpa;
     // No code to emit, so also no ctors to call
-    if (wasm.code_section_index == null) {
+    if (wasm.code_section_index == .none) {
         // Make sure to remove it from the resolved symbols so we do not emit
         // it within any section. TODO: Remove this once we implement garbage collection.
-        const loc = wasm.findGlobalSymbol("__wasm_call_ctors").?;
+        const loc = wasm.globals.get(wasm.preloaded_strings.__wasm_call_ctors).?;
         assert(wasm.resolved_symbols.swapRemove(loc));
         return;
     }
@@ -1965,7 +1967,7 @@ fn initializeCallCtorsFunction(wasm: *Wasm) !void {
     }
 
     try wasm.createSyntheticFunction(
-        "__wasm_call_ctors",
+        wasm.preloaded_strings.__wasm_call_ctors,
         std.wasm.Type{ .params = &.{}, .returns = &.{} },
         &function_body,
     );
@@ -1973,12 +1975,12 @@ fn initializeCallCtorsFunction(wasm: *Wasm) !void {
 
 fn createSyntheticFunction(
     wasm: *Wasm,
-    symbol_name: []const u8,
+    symbol_name: String,
     func_ty: std.wasm.Type,
     function_body: *std.ArrayList(u8),
 ) !void {
     const gpa = wasm.base.comp.gpa;
-    const loc = wasm.findGlobalSymbol(symbol_name).?; // forgot to create symbol?
+    const loc = wasm.globals.get(symbol_name).?;
     const symbol = wasm.symbolLocSymbol(loc);
     if (symbol.isDead()) {
         return;
@@ -1998,7 +2000,7 @@ fn createSyntheticFunction(
     const atom = wasm.getAtomPtr(atom_index);
     atom.size = @intCast(function_body.items.len);
     atom.code = function_body.moveToUnmanaged();
-    try wasm.appendAtomAtIndex(wasm.code_section_index.?, atom_index);
+    try wasm.appendAtomAtIndex(wasm.code_section_index.unwrap().?, atom_index);
 }
 
 /// Unlike `createSyntheticFunction` this function is to be called by
@@ -2016,7 +2018,7 @@ pub fn createFunction(
 
 /// If required, sets the function index in the `start` section.
 fn setupStartSection(wasm: *Wasm) !void {
-    if (wasm.findGlobalSymbol("__wasm_init_memory")) |loc| {
+    if (wasm.globals.get(wasm.preloaded_strings.__wasm_init_memory)) |loc| {
         wasm.entry = wasm.symbolLocSymbol(loc).index;
     }
 }
@@ -2029,7 +2031,7 @@ fn initializeTLSFunction(wasm: *Wasm) !void {
     if (!shared_memory) return;
 
     // ensure function is marked as we must emit it
-    wasm.symbolLocSymbol(wasm.findGlobalSymbol("__wasm_init_tls").?).mark();
+    wasm.symbolLocSymbol(wasm.globals.get(wasm.preloaded_strings.__wasm_init_tls).?).mark();
 
     var function_body = std.ArrayList(u8).init(gpa);
     defer function_body.deinit();
@@ -2041,14 +2043,14 @@ fn initializeTLSFunction(wasm: *Wasm) !void {
     // If there's a TLS segment, initialize it during runtime using the bulk-memory feature
     if (wasm.data_segments.getIndex(".tdata")) |data_index| {
         const segment_index = wasm.data_segments.entries.items(.value)[data_index];
-        const segment = wasm.segments.items[segment_index];
+        const segment = wasm.segmentPtr(segment_index);
 
         const param_local: u32 = 0;
 
         try writer.writeByte(std.wasm.opcode(.local_get));
         try leb.writeUleb128(writer, param_local);
 
-        const tls_base_loc = wasm.findGlobalSymbol("__tls_base").?;
+        const tls_base_loc = wasm.globals.get(wasm.preloaded_strings.__tls_base).?;
         try writer.writeByte(std.wasm.opcode(.global_set));
         try leb.writeUleb128(writer, wasm.symbolLocSymbol(tls_base_loc).index);
 
@@ -2076,7 +2078,7 @@ fn initializeTLSFunction(wasm: *Wasm) !void {
     // If we have to perform any TLS relocations, call the corresponding function
     // which performs all runtime TLS relocations. This is a synthetic function,
     // generated by the linker.
-    if (wasm.findGlobalSymbol("__wasm_apply_global_tls_relocs")) |loc| {
+    if (wasm.globals.get(wasm.preloaded_strings.__wasm_apply_global_tls_relocs)) |loc| {
         try writer.writeByte(std.wasm.opcode(.call));
         try leb.writeUleb128(writer, wasm.symbolLocSymbol(loc).index);
         wasm.symbolLocSymbol(loc).mark();
@@ -2085,7 +2087,7 @@ fn initializeTLSFunction(wasm: *Wasm) !void {
     try writer.writeByte(std.wasm.opcode(.end));
 
     try wasm.createSyntheticFunction(
-        "__wasm_init_tls",
+        wasm.preloaded_strings.__wasm_init_tls,
         std.wasm.Type{ .params = &.{.i32}, .returns = &.{} },
         &function_body,
     );
@@ -2101,21 +2103,18 @@ fn setupImports(wasm: *Wasm) !void {
         };
 
         const symbol = wasm.symbolLocSymbol(symbol_loc);
-        if (symbol.isDead() or
-            !symbol.requiresImport() or
-            std.mem.eql(u8, wasm.symbolLocName(symbol_loc), "__indirect_function_table"))
-        {
-            continue;
-        }
+        if (symbol.isDead()) continue;
+        if (!symbol.requiresImport()) continue;
+        if (symbol.name == wasm.preloaded_strings.__indirect_function_table) continue;
 
-        log.debug("Symbol '{s}' will be imported from the host", .{wasm.symbolLocName(symbol_loc)});
+        log.debug("Symbol '{s}' will be imported from the host", .{wasm.stringSlice(symbol.name)});
         const import = objectImport(wasm, object_id, 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: Import = .{
-            .module_name = try wasm.string_table.put(gpa, objectString(wasm, object_id, import.module_name)),
-            .name = try wasm.string_table.put(gpa, objectString(wasm, object_id, import.name)),
+            .module_name = import.module_name,
+            .name = import.name,
             .kind = import.kind,
         };
         // TODO: De-duplicate imports when they contain the same names and type
@@ -2283,7 +2282,8 @@ fn checkExportNames(wasm: *Wasm) !void {
         var failed_exports = false;
 
         for (force_exp_names) |exp_name| {
-            const loc = wasm.findGlobalSymbol(exp_name) orelse {
+            const exp_name_interned = try wasm.internString(exp_name);
+            const loc = wasm.globals.get(exp_name_interned) orelse {
                 var err = try diags.addErrorWithNotes(0);
                 try err.addMsg("could not export '{s}', symbol not found", .{exp_name});
                 failed_exports = true;
@@ -2310,11 +2310,6 @@ fn setupExports(wasm: *Wasm) !void {
         const symbol = wasm.symbolLocSymbol(sym_loc);
         if (!symbol.isExported(comp.config.rdynamic)) continue;
 
-        const sym_name = wasm.symbolLocName(sym_loc);
-        const export_name = if (sym_loc.file == .none)
-            symbol.name
-        else
-            try wasm.string_table.put(gpa, sym_name);
         const exp: Export = if (symbol.tag == .data) exp: {
             const global_index = @as(u32, @intCast(wasm.imported_globals_count + wasm.wasm_globals.items.len));
             try wasm.wasm_globals.append(gpa, .{
@@ -2322,18 +2317,18 @@ fn setupExports(wasm: *Wasm) !void {
                 .init = .{ .i32_const = @as(i32, @intCast(symbol.virtual_address)) },
             });
             break :exp .{
-                .name = export_name,
+                .name = symbol.name,
                 .kind = .global,
                 .index = global_index,
             };
         } else .{
-            .name = export_name,
+            .name = symbol.name,
             .kind = symbol.tag.externalType(),
             .index = symbol.index,
         };
         log.debug("Exporting symbol '{s}' as '{s}' at index: ({d})", .{
-            sym_name,
-            wasm.string_table.get(exp.name),
+            wasm.stringSlice(symbol.name),
+            wasm.stringSlice(exp.name),
             exp.index,
         });
         try wasm.exports.append(gpa, exp);
@@ -2346,20 +2341,18 @@ fn setupStart(wasm: *Wasm) !void {
     const comp = wasm.base.comp;
     const diags = &wasm.base.comp.link_diags;
     // do not export entry point if user set none or no default was set.
-    const entry_name = wasm.entry_name orelse return;
+    const entry_name = wasm.entry_name.unwrap() orelse return;
 
-    const symbol_loc = wasm.findGlobalSymbol(entry_name) orelse {
-        var err = try diags.addErrorWithNotes(0);
-        try err.addMsg("Entry symbol '{s}' missing, use '-fno-entry' to suppress", .{entry_name});
-        return error.FlushFailure;
+    const symbol_loc = wasm.globals.get(entry_name) orelse {
+        var err = try diags.addErrorWithNotes(1);
+        try err.addMsg("entry symbol '{s}' missing", .{wasm.stringSlice(entry_name)});
+        try err.addNote("'-fno-entry' suppresses this error", .{});
+        return error.LinkFailure;
     };
 
     const symbol = wasm.symbolLocSymbol(symbol_loc);
-    if (symbol.tag != .function) {
-        var err = try diags.addErrorWithNotes(0);
-        try err.addMsg("Entry symbol '{s}' is not a function", .{entry_name});
-        return error.FlushFailure;
-    }
+    if (symbol.tag != .function)
+        return diags.fail("entry symbol '{s}' is not a function", .{wasm.stringSlice(entry_name)});
 
     // Ensure the symbol is exported so host environment can access it
     if (comp.config.output_mode != .Obj) {
@@ -2387,7 +2380,7 @@ fn setupMemory(wasm: *Wasm) !void {
 
     const is_obj = comp.config.output_mode == .Obj;
 
-    const stack_ptr = if (wasm.findGlobalSymbol("__stack_pointer")) |loc| index: {
+    const stack_ptr = if (wasm.globals.get(wasm.preloaded_strings.__stack_pointer)) |loc| index: {
         const sym = wasm.symbolLocSymbol(loc);
         break :index sym.index - wasm.imported_globals_count;
     } else null;
@@ -2404,20 +2397,20 @@ fn setupMemory(wasm: *Wasm) !void {
     var offset: u32 = @as(u32, @intCast(memory_ptr));
     var data_seg_it = wasm.data_segments.iterator();
     while (data_seg_it.next()) |entry| {
-        const segment = &wasm.segments.items[entry.value_ptr.*];
+        const segment = wasm.segmentPtr(entry.value_ptr.*);
         memory_ptr = segment.alignment.forward(memory_ptr);
 
         // set TLS-related symbols
         if (mem.eql(u8, entry.key_ptr.*, ".tdata")) {
-            if (wasm.findGlobalSymbol("__tls_size")) |loc| {
+            if (wasm.globals.get(wasm.preloaded_strings.__tls_size)) |loc| {
                 const sym = wasm.symbolLocSymbol(loc);
                 wasm.wasm_globals.items[sym.index - wasm.imported_globals_count].init.i32_const = @intCast(segment.size);
             }
-            if (wasm.findGlobalSymbol("__tls_align")) |loc| {
+            if (wasm.globals.get(wasm.preloaded_strings.__tls_align)) |loc| {
                 const sym = wasm.symbolLocSymbol(loc);
                 wasm.wasm_globals.items[sym.index - wasm.imported_globals_count].init.i32_const = @intCast(segment.alignment.toByteUnits().?);
             }
-            if (wasm.findGlobalSymbol("__tls_base")) |loc| {
+            if (wasm.globals.get(wasm.preloaded_strings.__tls_base)) |loc| {
                 const sym = wasm.symbolLocSymbol(loc);
                 wasm.wasm_globals.items[sym.index - wasm.imported_globals_count].init.i32_const = if (shared_memory)
                     @as(i32, 0)
@@ -2435,7 +2428,7 @@ fn setupMemory(wasm: *Wasm) !void {
     if (shared_memory and wasm.hasPassiveInitializationSegments()) {
         // align to pointer size
         memory_ptr = mem.alignForward(u64, memory_ptr, 4);
-        const loc = try wasm.createSyntheticSymbol("__wasm_init_memory_flag", .data);
+        const loc = try wasm.createSyntheticSymbol(wasm.preloaded_strings.__wasm_init_memory_flag, .data);
         const sym = wasm.symbolLocSymbol(loc);
         sym.mark();
         sym.virtual_address = @as(u32, @intCast(memory_ptr));
@@ -2452,7 +2445,7 @@ fn setupMemory(wasm: *Wasm) !void {
 
     // One of the linked object files has a reference to the __heap_base symbol.
     // We must set its virtual address so it can be used in relocations.
-    if (wasm.findGlobalSymbol("__heap_base")) |loc| {
+    if (wasm.globals.get(wasm.preloaded_strings.__heap_base)) |loc| {
         const symbol = wasm.symbolLocSymbol(loc);
         symbol.virtual_address = @intCast(heap_alignment.forward(memory_ptr));
     }
@@ -2482,7 +2475,7 @@ fn setupMemory(wasm: *Wasm) !void {
     wasm.memories.limits.min = @as(u32, @intCast(memory_ptr / page_size));
     log.debug("Total memory pages: {d}", .{wasm.memories.limits.min});
 
-    if (wasm.findGlobalSymbol("__heap_end")) |loc| {
+    if (wasm.globals.get(wasm.preloaded_strings.__heap_end)) |loc| {
         const symbol = wasm.symbolLocSymbol(loc);
         symbol.virtual_address = @as(u32, @intCast(memory_ptr));
     }
@@ -2512,12 +2505,12 @@ 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_id: ObjectId, symbol_index: Symbol.Index) !u32 {
+pub fn getMatchingSegment(wasm: *Wasm, object_id: ObjectId, symbol_index: Symbol.Index) !Segment.Index {
     const comp = wasm.base.comp;
     const gpa = comp.gpa;
     const diags = &wasm.base.comp.link_diags;
     const symbol = objectSymbols(wasm, object_id)[@intFromEnum(symbol_index)];
-    const index: u32 = @intCast(wasm.segments.items.len);
+    const index: Segment.Index = @enumFromInt(wasm.segments.items.len);
     const shared_memory = comp.config.shared_memory;
 
     switch (symbol.tag) {
@@ -2545,66 +2538,27 @@ pub fn getMatchingSegment(wasm: *Wasm, object_id: ObjectId, symbol_index: Symbol
                 return index;
             } else return result.value_ptr.*;
         },
-        .function => return wasm.code_section_index orelse blk: {
-            wasm.code_section_index = index;
+        .function => return wasm.code_section_index.unwrap() orelse blk: {
+            wasm.code_section_index = index.toOptional();
             try wasm.appendDummySegment();
             break :blk index;
         },
         .section => {
-            const section_name = objectSymbolName(wasm, object_id, symbol_index);
-            if (mem.eql(u8, section_name, ".debug_info")) {
-                return wasm.debug_info_index orelse blk: {
-                    wasm.debug_info_index = index;
-                    try wasm.appendDummySegment();
-                    break :blk index;
-                };
-            } else if (mem.eql(u8, section_name, ".debug_line")) {
-                return wasm.debug_line_index orelse blk: {
-                    wasm.debug_line_index = index;
-                    try wasm.appendDummySegment();
-                    break :blk index;
-                };
-            } else if (mem.eql(u8, section_name, ".debug_loc")) {
-                return wasm.debug_loc_index orelse blk: {
-                    wasm.debug_loc_index = index;
-                    try wasm.appendDummySegment();
-                    break :blk index;
-                };
-            } else if (mem.eql(u8, section_name, ".debug_ranges")) {
-                return wasm.debug_ranges_index orelse blk: {
-                    wasm.debug_ranges_index = index;
-                    try wasm.appendDummySegment();
-                    break :blk index;
-                };
-            } else if (mem.eql(u8, section_name, ".debug_pubnames")) {
-                return wasm.debug_pubnames_index orelse blk: {
-                    wasm.debug_pubnames_index = index;
-                    try wasm.appendDummySegment();
-                    break :blk index;
-                };
-            } else if (mem.eql(u8, section_name, ".debug_pubtypes")) {
-                return wasm.debug_pubtypes_index orelse blk: {
-                    wasm.debug_pubtypes_index = index;
-                    try wasm.appendDummySegment();
-                    break :blk index;
-                };
-            } else if (mem.eql(u8, section_name, ".debug_abbrev")) {
-                return wasm.debug_abbrev_index orelse blk: {
-                    wasm.debug_abbrev_index = index;
-                    try wasm.appendDummySegment();
-                    break :blk index;
-                };
-            } else if (mem.eql(u8, section_name, ".debug_str")) {
-                return wasm.debug_str_index orelse blk: {
-                    wasm.debug_str_index = index;
-                    try wasm.appendDummySegment();
-                    break :blk index;
-                };
+            const section_name = wasm.objectSymbol(object_id, symbol_index).name;
+
+            inline for (@typeInfo(CustomSections).@"struct".fields) |field| {
+                if (@field(wasm.custom_sections, field.name).name == section_name) {
+                    const field_ptr = &@field(wasm.custom_sections, field.name).index;
+                    return field_ptr.unwrap() orelse {
+                        field_ptr.* = index.toOptional();
+                        try wasm.appendDummySegment();
+                        return index;
+                    };
+                }
             } else {
-                var err = try diags.addErrorWithNotes(1);
-                try err.addMsg("found unknown section '{s}'", .{section_name});
-                try err.addNote("defined in '{'}'", .{objectPath(wasm, object_id)});
-                return error.UnexpectedValue;
+                return diags.failParse(objectPath(wasm, object_id), "unknown section: {s}", .{
+                    wasm.stringSlice(section_name),
+                });
             }
         },
         else => unreachable,
@@ -2803,10 +2757,9 @@ fn writeToFile(
         }
 
         if (import_memory) {
-            const mem_name = if (is_obj) "__linear_memory" else "memory";
             const mem_imp: Import = .{
-                .module_name = try wasm.string_table.put(gpa, wasm.host_name),
-                .name = try wasm.string_table.put(gpa, mem_name),
+                .module_name = wasm.host_name,
+                .name = if (is_obj) wasm.preloaded_strings.__linear_memory else wasm.preloaded_strings.memory,
                 .kind = .{ .memory = wasm.memories.limits },
             };
             try wasm.emitImport(binary_writer, mem_imp);
@@ -2898,7 +2851,7 @@ fn writeToFile(
         const header_offset = try reserveVecSectionHeader(&binary_bytes);
 
         for (wasm.exports.items) |exp| {
-            const name = wasm.string_table.get(exp.name);
+            const name = wasm.stringSlice(exp.name);
             try leb.writeUleb128(binary_writer, @as(u32, @intCast(name.len)));
             try binary_writer.writeAll(name);
             try leb.writeUleb128(binary_writer, @intFromEnum(exp.kind));
@@ -2937,7 +2890,7 @@ fn writeToFile(
     if (wasm.function_table.count() > 0) {
         const header_offset = try reserveVecSectionHeader(&binary_bytes);
 
-        const table_loc = wasm.findGlobalSymbol("__indirect_function_table").?;
+        const table_loc = wasm.globals.get(wasm.preloaded_strings.__indirect_function_table).?;
         const table_sym = wasm.symbolLocSymbol(table_loc);
 
         const flags: u32 = if (table_sym.index == 0) 0x0 else 0x02; // passive with implicit 0-index table or set table index manually
@@ -2982,7 +2935,7 @@ fn writeToFile(
     }
 
     // Code section
-    if (wasm.code_section_index != null) {
+    if (wasm.code_section_index != .none) {
         const header_offset = try reserveVecSectionHeader(&binary_bytes);
         const start_offset = binary_bytes.items.len - 5; // minus 5 so start offset is 5 to include entry count
 
@@ -3022,7 +2975,7 @@ fn writeToFile(
             // want to guarantee the data is zero initialized
             if (!import_memory and std.mem.eql(u8, entry.key_ptr.*, ".bss")) continue;
             const segment_index = entry.value_ptr.*;
-            const segment = wasm.segments.items[segment_index];
+            const segment = wasm.segmentPtr(segment_index);
             if (segment.size == 0) continue; // do not emit empty segments
             segment_count += 1;
             var atom_index = wasm.atoms.get(segment_index).?;
@@ -3133,24 +3086,8 @@ fn writeToFile(
         var debug_bytes = std.ArrayList(u8).init(gpa);
         defer debug_bytes.deinit();
 
-        const DebugSection = struct {
-            name: []const u8,
-            index: ?u32,
-        };
-
-        const debug_sections: []const DebugSection = &.{
-            .{ .name = ".debug_info", .index = wasm.debug_info_index },
-            .{ .name = ".debug_pubtypes", .index = wasm.debug_pubtypes_index },
-            .{ .name = ".debug_abbrev", .index = wasm.debug_abbrev_index },
-            .{ .name = ".debug_line", .index = wasm.debug_line_index },
-            .{ .name = ".debug_str", .index = wasm.debug_str_index },
-            .{ .name = ".debug_pubnames", .index = wasm.debug_pubnames_index },
-            .{ .name = ".debug_loc", .index = wasm.debug_loc_index },
-            .{ .name = ".debug_ranges", .index = wasm.debug_ranges_index },
-        };
-
-        for (debug_sections) |item| {
-            if (item.index) |index| {
+        inline for (@typeInfo(CustomSections).@"struct".fields) |field| {
+            if (@field(wasm.custom_sections, field.name).index.unwrap()) |index| {
                 var atom = wasm.getAtomPtr(wasm.atoms.get(index).?);
                 while (true) {
                     atom.resolveRelocs(wasm);
@@ -3158,7 +3095,7 @@ fn writeToFile(
                     if (atom.prev == .null) break;
                     atom = wasm.getAtomPtr(atom.prev);
                 }
-                try emitDebugSection(&binary_bytes, debug_bytes.items, item.name);
+                try emitDebugSection(&binary_bytes, debug_bytes.items, field.name);
                 debug_bytes.clearRetainingCapacity();
             }
         }
@@ -3430,11 +3367,11 @@ fn emitInit(writer: anytype, init_expr: std.wasm.InitExpression) !void {
 }
 
 fn emitImport(wasm: *Wasm, writer: anytype, import: Import) !void {
-    const module_name = wasm.string_table.get(import.module_name);
+    const module_name = wasm.stringSlice(import.module_name);
     try leb.writeUleb128(writer, @as(u32, @intCast(module_name.len)));
     try writer.writeAll(module_name);
 
-    const name = wasm.string_table.get(import.name);
+    const name = wasm.stringSlice(import.name);
     try leb.writeUleb128(writer, @as(u32, @intCast(name.len)));
     try writer.writeAll(name);
 
@@ -3515,7 +3452,7 @@ fn linkWithLLD(wasm: *Wasm, arena: Allocator, tid: Zcu.PerThread.Id, prog_node:
         }
         try man.addOptionalFile(module_obj_path);
         try man.addOptionalFilePath(compiler_rt_path);
-        man.hash.addOptionalBytes(wasm.entry_name);
+        man.hash.addOptionalBytes(wasm.optionalStringSlice(wasm.entry_name));
         man.hash.add(wasm.base.stack_size);
         man.hash.add(wasm.base.build_id);
         man.hash.add(import_memory);
@@ -3664,7 +3601,7 @@ fn linkWithLLD(wasm: *Wasm, arena: Allocator, tid: Zcu.PerThread.Id, prog_node:
             try argv.append("--export-dynamic");
         }
 
-        if (wasm.entry_name) |entry_name| {
+        if (wasm.optionalStringSlice(wasm.entry_name)) |entry_name| {
             try argv.appendSlice(&.{ "--entry", entry_name });
         } else {
             try argv.append("--no-entry");
@@ -4009,7 +3946,7 @@ fn emitCodeRelocations(
     section_index: u32,
     symbol_table: std.AutoArrayHashMap(SymbolLoc, u32),
 ) !void {
-    const code_index = wasm.code_section_index orelse return;
+    const code_index = wasm.code_section_index.unwrap() orelse return;
     const writer = binary_bytes.writer();
     const header_offset = try reserveCustomSectionHeader(binary_bytes);
 
@@ -4106,7 +4043,7 @@ fn hasPassiveInitializationSegments(wasm: *const Wasm) bool {
 
     var it = wasm.data_segments.iterator();
     while (it.next()) |entry| {
-        const segment: Segment = wasm.segments.items[entry.value_ptr.*];
+        const segment = wasm.segmentPtr(entry.value_ptr.*);
         if (segment.needsPassiveInitialization(import_memory, entry.key_ptr.*)) {
             return true;
         }
@@ -4213,10 +4150,13 @@ fn mark(wasm: *Wasm, loc: SymbolLoc) !void {
     }
 }
 
-fn defaultEntrySymbolName(wasi_exec_model: std.builtin.WasiExecModel) []const u8 {
+fn defaultEntrySymbolName(
+    preloaded_strings: *const PreloadedStrings,
+    wasi_exec_model: std.builtin.WasiExecModel,
+) String {
     return switch (wasi_exec_model) {
-        .reactor => "_initialize",
-        .command => "_start",
+        .reactor => preloaded_strings._initialize,
+        .command => preloaded_strings._start,
     };
 }
 
@@ -4352,7 +4292,7 @@ pub const Atom = struct {
             symbol.tag != .section and
             symbol.isDead())
         {
-            const val = atom.thombstone(wasm) orelse relocation.addend;
+            const val = atom.tombstone(wasm) orelse relocation.addend;
             return @bitCast(val);
         }
         switch (relocation.relocation_type) {
@@ -4394,7 +4334,7 @@ pub const Atom = struct {
             },
             .R_WASM_FUNCTION_OFFSET_I32 => {
                 if (symbol.isUndefined()) {
-                    const val = atom.thombstone(wasm) orelse relocation.addend;
+                    const val = atom.tombstone(wasm) orelse relocation.addend;
                     return @bitCast(val);
                 }
                 const target_atom_index = wasm.symbol_atom.get(target_loc).?;
@@ -4411,16 +4351,19 @@ pub const Atom = struct {
         }
     }
 
-    // For a given `Atom` returns whether it has a thombstone value or not.
+    // For a given `Atom` returns whether it has a tombstone value or not.
     /// This defines whether we want a specific value when a section is dead.
-    fn thombstone(atom: Atom, wasm: *const Wasm) ?i64 {
-        const atom_name = wasm.symbolLocName(atom.symbolLoc());
-        if (std.mem.eql(u8, atom_name, ".debug_ranges") or std.mem.eql(u8, atom_name, ".debug_loc")) {
+    fn tombstone(atom: Atom, wasm: *const Wasm) ?i64 {
+        const atom_name = wasm.symbolLocSymbol(atom.symbolLoc()).name;
+        if (atom_name == wasm.custom_sections.@".debug_ranges".name or
+            atom_name == wasm.custom_sections.@".debug_loc".name)
+        {
             return -2;
-        } else if (std.mem.startsWith(u8, atom_name, ".debug_")) {
+        } else if (std.mem.startsWith(u8, wasm.stringSlice(atom_name), ".debug_")) {
             return -1;
+        } else {
+            return null;
         }
-        return null;
     }
 };
 
@@ -4509,8 +4452,8 @@ pub const Relocation = struct {
 /// of the import using offsets into a string table, rather than the slices itself.
 /// This saves us (potentially) 24 bytes per import on 64bit machines.
 pub const Import = struct {
-    module_name: u32,
-    name: u32,
+    module_name: String,
+    name: String,
     kind: std.wasm.Import.Kind,
 };
 
@@ -4519,7 +4462,7 @@ pub const Import = struct {
 /// of the export using offsets into a string table, rather than the slice itself.
 /// This saves us (potentially) 12 bytes per export on 64bit machines.
 pub const Export = struct {
-    name: u32,
+    name: String,
     index: u32,
     kind: std.wasm.ExternalKind,
 };
@@ -4719,7 +4662,7 @@ fn parseSymbolIntoAtom(wasm: *Wasm, object_id: ObjectId, symbol_index: Symbol.In
     atom.code = std.ArrayListUnmanaged(u8).fromOwnedSlice(relocatable_data.data[0..relocatable_data.size]);
     atom.original_offset = relocatable_data.offset;
 
-    const segment: *Wasm.Segment = &wasm.segments.items[final_index];
+    const segment = wasm.segmentPtr(final_index);
     if (relocatable_data.type == .data) { //code section and custom sections are 1-byte aligned
         segment.alignment = segment.alignment.max(atom.alignment);
     }
@@ -4782,3 +4725,48 @@ fn searchRelocEnd(relocs: []const Wasm.Relocation, address: u32) usize {
     }
     return relocs.len;
 }
+
+pub fn internString(wasm: *Wasm, bytes: []const u8) error{OutOfMemory}!String {
+    const gpa = wasm.base.comp.gpa;
+    const gop = try wasm.string_table.getOrPutContextAdapted(
+        gpa,
+        @as([]const u8, bytes),
+        @as(String.TableIndexAdapter, .{ .bytes = wasm.string_bytes.items }),
+        @as(String.TableContext, .{ .bytes = wasm.string_bytes.items }),
+    );
+    if (gop.found_existing) return gop.key_ptr.*;
+
+    try wasm.string_bytes.ensureUnusedCapacity(gpa, bytes.len + 1);
+    const new_off: String = @enumFromInt(wasm.string_bytes.items.len);
+
+    wasm.string_bytes.appendSliceAssumeCapacity(bytes);
+    wasm.string_bytes.appendAssumeCapacity(0);
+
+    gop.key_ptr.* = new_off;
+
+    return new_off;
+}
+
+pub fn getExistingString(wasm: *const Wasm, bytes: []const u8) ?String {
+    return wasm.string_table.getKeyAdapted(bytes, @as(String.TableIndexAdapter, .{
+        .bytes = wasm.string_bytes.items,
+    }));
+}
+
+pub fn stringSlice(wasm: *const Wasm, index: String) [:0]const u8 {
+    const slice = wasm.string_bytes.items[@intFromEnum(index)..];
+    return slice[0..mem.indexOfScalar(u8, slice, 0).? :0];
+}
+
+pub fn optionalStringSlice(wasm: *const Wasm, index: OptionalString) ?[:0]const u8 {
+    return stringSlice(wasm, index.unwrap() orelse return null);
+}
+
+pub fn castToString(wasm: *const Wasm, index: u32) String {
+    assert(index == 0 or wasm.string_bytes.items[index - 1] == 0);
+    return @enumFromInt(index);
+}
+
+fn segmentPtr(wasm: *const Wasm, index: Segment.Index) *Segment {
+    return &wasm.segments.items[@intFromEnum(index)];
+}