Commit 5ac5cd9de7

Jakub Konka <kubkon@jakubkonka.com>
2021-06-23 15:11:31
zld: naively parse all dylib deps in stubs
1 parent 3cb6b6b
src/link/MachO/Archive.zig
@@ -8,12 +8,13 @@ const macho = std.macho;
 const mem = std.mem;
 
 const Allocator = mem.Allocator;
+const Arch = std.Target.Cpu.Arch;
 const Object = @import("Object.zig");
 
 usingnamespace @import("commands.zig");
 
 allocator: *Allocator,
-arch: ?std.Target.Cpu.Arch = null,
+arch: ?Arch = null,
 file: ?fs.File = null,
 header: ?ar_hdr = null,
 name: ?[]const u8 = null,
@@ -85,10 +86,36 @@ const ar_hdr = extern struct {
     }
 };
 
-pub fn init(allocator: *Allocator) Archive {
-    return .{
+pub fn createAndParseFromPath(allocator: *Allocator, arch: Arch, path: []const u8) !?*Archive {
+    const file = fs.cwd().openFile(path, .{}) catch |err| switch (err) {
+        error.FileNotFound => return null,
+        else => |e| return e,
+    };
+    errdefer file.close();
+
+    const archive = try allocator.create(Archive);
+    errdefer allocator.destroy(archive);
+
+    const name = try allocator.dupe(u8, path);
+    errdefer allocator.free(name);
+
+    archive.* = .{
         .allocator = allocator,
+        .arch = arch,
+        .name = name,
+        .file = file,
+    };
+
+    archive.parse() catch |err| switch (err) {
+        error.EndOfStream, error.NotArchive => {
+            archive.deinit();
+            allocator.destroy(archive);
+            return null;
+        },
+        else => |e| return e,
     };
+
+    return archive;
 }
 
 pub fn deinit(self: *Archive) void {
@@ -116,15 +143,15 @@ pub fn parse(self: *Archive) !void {
     const magic = try reader.readBytesNoEof(SARMAG);
 
     if (!mem.eql(u8, &magic, ARMAG)) {
-        log.err("invalid magic: expected '{s}', found '{s}'", .{ ARMAG, magic });
-        return error.MalformedArchive;
+        log.debug("invalid magic: expected '{s}', found '{s}'", .{ ARMAG, magic });
+        return error.NotArchive;
     }
 
     self.header = try reader.readStruct(ar_hdr);
 
     if (!mem.eql(u8, &self.header.?.ar_fmag, ARFMAG)) {
-        log.err("invalid header delimiter: expected '{s}', found '{s}'", .{ ARFMAG, self.header.?.ar_fmag });
-        return error.MalformedArchive;
+        log.debug("invalid header delimiter: expected '{s}', found '{s}'", .{ ARFMAG, self.header.?.ar_fmag });
+        return error.NotArchive;
     }
 
     var embedded_name = try parseName(self.allocator, self.header.?, reader);
@@ -222,23 +249,15 @@ pub fn parseObject(self: Archive, offset: u32) !*Object {
     var object = try self.allocator.create(Object);
     errdefer self.allocator.destroy(object);
 
-    object.* = Object.init(self.allocator);
-    object.arch = self.arch.?;
-    object.file = try fs.cwd().openFile(self.name.?, .{});
-    object.name = name;
-    object.file_offset = @intCast(u32, try reader.context.getPos());
+    object.* = .{
+        .allocator = self.allocator,
+        .arch = self.arch.?,
+        .file = try fs.cwd().openFile(self.name.?, .{}),
+        .name = name,
+        .file_offset = @intCast(u32, try reader.context.getPos()),
+    };
     try object.parse();
-
     try reader.context.seekTo(0);
 
     return object;
 }
-
-pub fn isArchive(file: fs.File) !bool {
-    const magic = file.reader().readBytesNoEof(Archive.SARMAG) catch |err| switch (err) {
-        error.EndOfStream => return false,
-        else => |e| return e,
-    };
-    try file.seekTo(0);
-    return mem.eql(u8, &magic, Archive.ARMAG);
-}
src/link/MachO/Dylib.zig
@@ -8,6 +8,7 @@ const macho = std.macho;
 const mem = std.mem;
 
 const Allocator = mem.Allocator;
+const Arch = std.Target.Cpu.Arch;
 const Symbol = @import("Symbol.zig");
 const LibStub = @import("../tapi.zig").LibStub;
 
@@ -15,10 +16,11 @@ usingnamespace @import("commands.zig");
 
 allocator: *Allocator,
 
-arch: ?std.Target.Cpu.Arch = null,
+arch: ?Arch = null,
 header: ?macho.mach_header_64 = null,
 file: ?fs.File = null,
 name: ?[]const u8 = null,
+syslibroot: ?[]const u8 = null,
 
 ordinal: ?u16 = null,
 
@@ -35,6 +37,11 @@ id: ?Id = null,
 /// a symbol is referenced by an object file.
 symbols: std.StringArrayHashMapUnmanaged(void) = .{},
 
+// TODO we should keep track of already parsed dylibs so that
+// we don't unnecessarily reparse them again.
+// TODO add dylib dep analysis and extraction for .dylib files.
+dylibs: std.ArrayListUnmanaged(*Dylib) = .{},
+
 pub const Id = struct {
     name: []const u8,
     timestamp: u32,
@@ -46,8 +53,57 @@ pub const Id = struct {
     }
 };
 
-pub fn init(allocator: *Allocator) Dylib {
-    return .{ .allocator = allocator };
+pub const Error = error{
+    OutOfMemory,
+    EmptyStubFile,
+    MismatchedCpuArchitecture,
+    UnsupportedCpuArchitecture,
+} || fs.File.OpenError || std.os.PReadError;
+
+pub fn createAndParseFromPath(
+    allocator: *Allocator,
+    arch: Arch,
+    path: []const u8,
+    syslibroot: ?[]const u8,
+    recurse_libs: bool,
+) Error!?*Dylib {
+    const file = fs.cwd().openFile(path, .{}) catch |err| switch (err) {
+        error.FileNotFound => return null,
+        else => |e| return e,
+    };
+    errdefer file.close();
+
+    const dylib = try allocator.create(Dylib);
+    errdefer allocator.destroy(dylib);
+
+    const name = try allocator.dupe(u8, path);
+    errdefer allocator.free(name);
+
+    dylib.* = .{
+        .allocator = allocator,
+        .arch = arch,
+        .name = name,
+        .file = file,
+        .syslibroot = syslibroot,
+    };
+
+    dylib.parse(recurse_libs) catch |err| switch (err) {
+        error.EndOfStream, error.NotDylib => {
+            try file.seekTo(0);
+
+            var lib_stub = LibStub.loadFromFile(allocator, file) catch {
+                dylib.deinit();
+                allocator.destroy(dylib);
+                return null;
+            };
+            defer lib_stub.deinit();
+
+            try dylib.parseFromStub(lib_stub, recurse_libs);
+        },
+        else => |e| return e,
+    };
+
+    return dylib;
 }
 
 pub fn deinit(self: *Dylib) void {
@@ -60,6 +116,7 @@ pub fn deinit(self: *Dylib) void {
         self.allocator.free(key);
     }
     self.symbols.deinit(self.allocator);
+    self.dylibs.deinit(self.allocator);
 
     if (self.name) |name| {
         self.allocator.free(name);
@@ -76,15 +133,15 @@ pub fn closeFile(self: Dylib) void {
     }
 }
 
-pub fn parse(self: *Dylib) !void {
+pub fn parse(self: *Dylib, recurse_libs: bool) !void {
     log.debug("parsing shared library '{s}'", .{self.name.?});
 
     var reader = self.file.?.reader();
     self.header = try reader.readStruct(macho.mach_header_64);
 
     if (self.header.?.filetype != macho.MH_DYLIB) {
-        log.err("invalid filetype: expected 0x{x}, found 0x{x}", .{ macho.MH_DYLIB, self.header.?.filetype });
-        return error.MalformedDylib;
+        log.debug("invalid filetype: expected 0x{x}, found 0x{x}", .{ macho.MH_DYLIB, self.header.?.filetype });
+        return error.NotDylib;
     }
 
     const this_arch: std.Target.Cpu.Arch = switch (self.header.?.cputype) {
@@ -190,7 +247,7 @@ fn addObjCClassSymbols(self: *Dylib, sym_name: []const u8) !void {
     }
 }
 
-pub fn parseFromStub(self: *Dylib, lib_stub: LibStub) !void {
+pub fn parseFromStub(self: *Dylib, lib_stub: LibStub, recurse_libs: bool) !void {
     if (lib_stub.inner.len == 0) return error.EmptyStubFile;
 
     log.debug("parsing shared library from stub '{s}'", .{self.name.?});
@@ -236,9 +293,17 @@ pub fn parseFromStub(self: *Dylib, lib_stub: LibStub) !void {
             for (reexports) |reexp| {
                 if (!hasTarget(reexp.targets, target_string)) continue;
 
-                for (reexp.symbols) |sym_name| {
-                    if (self.symbols.contains(sym_name)) continue;
-                    try self.symbols.putNoClobber(self.allocator, try self.allocator.dupe(u8, sym_name), {});
+                if (reexp.symbols) |symbols| {
+                    for (symbols) |sym_name| {
+                        if (self.symbols.contains(sym_name)) continue;
+                        try self.symbols.putNoClobber(self.allocator, try self.allocator.dupe(u8, sym_name), {});
+                    }
+                }
+
+                if (reexp.objc_classes) |classes| {
+                    for (classes) |sym_name| {
+                        try self.addObjCClassSymbols(sym_name);
+                    }
                 }
             }
         }
@@ -249,6 +314,60 @@ pub fn parseFromStub(self: *Dylib, lib_stub: LibStub) !void {
             }
         }
     }
+
+    for (lib_stub.inner) |stub| {
+        if (!hasTarget(stub.targets, target_string)) continue;
+
+        if (stub.reexported_libraries) |reexports| reexports: {
+            if (!recurse_libs) break :reexports;
+
+            for (reexports) |reexp| {
+                if (!hasTarget(reexp.targets, target_string)) continue;
+
+                outer: for (reexp.libraries) |lib| {
+                    const dirname = fs.path.dirname(lib) orelse {
+                        log.warn("unable to resolve dependency {s}", .{lib});
+                        continue;
+                    };
+                    const filename = fs.path.basename(lib);
+                    const without_ext = if (mem.lastIndexOfScalar(u8, filename, '.')) |index|
+                        filename[0..index]
+                    else
+                        filename;
+
+                    for (&[_][]const u8{ "dylib", "tbd" }) |ext| {
+                        const with_ext = try std.fmt.allocPrint(self.allocator, "{s}.{s}", .{
+                            without_ext,
+                            ext,
+                        });
+                        defer self.allocator.free(with_ext);
+
+                        const lib_path = if (self.syslibroot) |syslibroot|
+                            try fs.path.join(self.allocator, &.{ syslibroot, dirname, with_ext })
+                        else
+                            try fs.path.join(self.allocator, &.{ dirname, with_ext });
+
+                        log.debug("trying dependency at fully resolved path {s}", .{lib_path});
+
+                        const dylib = (try createAndParseFromPath(
+                            self.allocator,
+                            self.arch.?,
+                            lib_path,
+                            self.syslibroot,
+                            true,
+                        )) orelse {
+                            continue;
+                        };
+
+                        try self.dylibs.append(self.allocator, dylib);
+                        continue :outer;
+                    } else {
+                        log.warn("unable to resolve dependency {s}", .{lib});
+                    }
+                }
+            }
+        }
+    }
 }
 
 fn hasTarget(targets: []const []const u8, target: []const u8) bool {
@@ -258,15 +377,6 @@ fn hasTarget(targets: []const []const u8, target: []const u8) bool {
     return false;
 }
 
-pub fn isDylib(file: fs.File) !bool {
-    const header = file.reader().readStruct(macho.mach_header_64) catch |err| switch (err) {
-        error.EndOfStream => return false,
-        else => |e| return e,
-    };
-    try file.seekTo(0);
-    return header.filetype == macho.MH_DYLIB;
-}
-
 pub fn createProxy(self: *Dylib, sym_name: []const u8) !?*Symbol {
     if (!self.symbols.contains(sym_name)) return null;
 
src/link/MachO/Object.zig
@@ -11,6 +11,7 @@ const mem = std.mem;
 const reloc = @import("reloc.zig");
 
 const Allocator = mem.Allocator;
+const Arch = std.Target.Cpu.Arch;
 const Relocation = reloc.Relocation;
 const Symbol = @import("Symbol.zig");
 const parseName = @import("Zld.zig").parseName;
@@ -18,7 +19,7 @@ const parseName = @import("Zld.zig").parseName;
 usingnamespace @import("commands.zig");
 
 allocator: *Allocator,
-arch: ?std.Target.Cpu.Arch = null,
+arch: ?Arch = null,
 header: ?macho.mach_header_64 = null,
 file: ?fs.File = null,
 file_offset: ?u32 = null,
@@ -173,10 +174,36 @@ const DebugInfo = struct {
     }
 };
 
-pub fn init(allocator: *Allocator) Object {
-    return .{
+pub fn createAndParseFromPath(allocator: *Allocator, arch: Arch, path: []const u8) !?*Object {
+    const file = fs.cwd().openFile(path, .{}) catch |err| switch (err) {
+        error.FileNotFound => return null,
+        else => |e| return e,
+    };
+    errdefer file.close();
+
+    const object = try allocator.create(Object);
+    errdefer allocator.destroy(object);
+
+    const name = try allocator.dupe(u8, path);
+    errdefer allocator.free(name);
+
+    object.* = .{
         .allocator = allocator,
+        .arch = arch,
+        .name = name,
+        .file = file,
     };
+
+    object.parse() catch |err| switch (err) {
+        error.EndOfStream, error.NotObject => {
+            object.deinit();
+            allocator.destroy(object);
+            return null;
+        },
+        else => |e| return e,
+    };
+
+    return object;
 }
 
 pub fn deinit(self: *Object) void {
@@ -223,11 +250,15 @@ pub fn parse(self: *Object) !void {
     self.header = try reader.readStruct(macho.mach_header_64);
 
     if (self.header.?.filetype != macho.MH_OBJECT) {
-        log.err("invalid filetype: expected 0x{x}, found 0x{x}", .{ macho.MH_OBJECT, self.header.?.filetype });
-        return error.MalformedObject;
+        log.debug("invalid filetype: expected 0x{x}, found 0x{x}", .{
+            macho.MH_OBJECT,
+            self.header.?.filetype,
+        });
+
+        return error.NotObject;
     }
 
-    const this_arch: std.Target.Cpu.Arch = switch (self.header.?.cputype) {
+    const this_arch: Arch = switch (self.header.?.cputype) {
         macho.CPU_TYPE_ARM64 => .aarch64,
         macho.CPU_TYPE_X86_64 => .x86_64,
         else => |value| {
@@ -533,12 +564,3 @@ pub fn parseDataInCode(self: *Object) !void {
         try self.data_in_code_entries.append(self.allocator, dice);
     }
 }
-
-pub fn isObject(file: fs.File) !bool {
-    const header = file.reader().readStruct(macho.mach_header_64) catch |err| switch (err) {
-        error.EndOfStream => return false,
-        else => |e| return e,
-    };
-    try file.seekTo(0);
-    return header.filetype == macho.MH_OBJECT;
-}
src/link/MachO/Zld.zig
@@ -16,7 +16,6 @@ const Allocator = mem.Allocator;
 const Archive = @import("Archive.zig");
 const CodeSignature = @import("CodeSignature.zig");
 const Dylib = @import("Dylib.zig");
-const LibStub = @import("../tapi.zig").LibStub;
 const Object = @import("Object.zig");
 const Symbol = @import("Symbol.zig");
 const Trie = @import("Trie.zig");
@@ -33,6 +32,7 @@ out_path: ?[]const u8 = null,
 
 // TODO these args will become obselete once Zld is coalesced with incremental
 // linker.
+syslibroot: ?[]const u8 = null,
 stack_size: u64 = 0,
 
 objects: std.ArrayListUnmanaged(*Object) = .{},
@@ -257,214 +257,90 @@ pub fn link(self: *Zld, files: []const []const u8, out_path: []const u8, args: L
 }
 
 fn parseInputFiles(self: *Zld, files: []const []const u8) !void {
-    const Input = struct {
-        kind: union(enum) {
-            object: fs.File,
-            archive: fs.File,
-            dylib: fs.File,
-            stub: LibStub,
-        },
-        name: []const u8,
-
-        fn deinit(input: *@This()) void {
-            switch (input.kind) {
-                .stub => |*stub| {
-                    stub.deinit();
-                },
-                else => {},
-            }
-        }
-    };
-    var classified = std.ArrayList(Input).init(self.allocator);
-    defer {
-        for (classified.items) |*input| {
-            input.deinit();
-        }
-        classified.deinit();
-    }
-
-    // First, classify input files: object, archive, dylib or stub (tbd).
     for (files) |file_name| {
-        const file = try fs.cwd().openFile(file_name, .{});
         const full_path = full_path: {
             var buffer: [std.fs.MAX_PATH_BYTES]u8 = undefined;
             const path = try std.fs.realpath(file_name, &buffer);
             break :full_path try self.allocator.dupe(u8, path);
         };
 
-        try_object: {
-            if (!(try Object.isObject(file))) break :try_object;
-            try classified.append(.{
-                .kind = .{ .object = file },
-                .name = full_path,
-            });
-            continue;
-        }
-
-        try_archive: {
-            if (!(try Archive.isArchive(file))) break :try_archive;
-            try classified.append(.{
-                .kind = .{ .archive = file },
-                .name = full_path,
-            });
+        if (try Object.createAndParseFromPath(self.allocator, self.arch.?, full_path)) |object| {
+            try self.objects.append(self.allocator, object);
             continue;
         }
 
-        try_dylib: {
-            if (!(try Dylib.isDylib(file))) break :try_dylib;
-            try classified.append(.{
-                .kind = .{ .dylib = file },
-                .name = full_path,
-            });
+        if (try Archive.createAndParseFromPath(self.allocator, self.arch.?, full_path)) |archive| {
+            try self.archives.append(self.allocator, archive);
             continue;
         }
 
-        try_stub: {
-            var lib_stub = LibStub.loadFromFile(self.allocator, file) catch {
-                break :try_stub;
-            };
-            try classified.append(.{
-                .kind = .{ .stub = lib_stub },
-                .name = full_path,
-            });
-            file.close();
+        if (try Dylib.createAndParseFromPath(
+            self.allocator,
+            self.arch.?,
+            full_path,
+            self.syslibroot,
+            true,
+        )) |dylib| {
+            try self.dylibs.append(self.allocator, dylib);
             continue;
         }
 
-        file.close();
         log.warn("unknown filetype for positional input file: '{s}'", .{file_name});
     }
-
-    // Based on our classification, proceed with parsing.
-    for (classified.items) |input| {
-        switch (input.kind) {
-            .object => |file| {
-                const object = try self.allocator.create(Object);
-                errdefer self.allocator.destroy(object);
-
-                object.* = Object.init(self.allocator);
-                object.arch = self.arch.?;
-                object.name = input.name;
-                object.file = file;
-
-                try object.parse();
-                try self.objects.append(self.allocator, object);
-            },
-            .archive => |file| {
-                const archive = try self.allocator.create(Archive);
-                errdefer self.allocator.destroy(archive);
-
-                archive.* = Archive.init(self.allocator);
-                archive.arch = self.arch.?;
-                archive.name = input.name;
-                archive.file = file;
-
-                try archive.parse();
-                try self.archives.append(self.allocator, archive);
-            },
-            .dylib, .stub => {
-                const dylib = try self.allocator.create(Dylib);
-                errdefer self.allocator.destroy(dylib);
-
-                dylib.* = Dylib.init(self.allocator);
-                dylib.arch = self.arch.?;
-                dylib.name = input.name;
-
-                if (input.kind == .dylib) {
-                    dylib.file = input.kind.dylib;
-                    try dylib.parse();
-                } else {
-                    try dylib.parseFromStub(input.kind.stub);
-                }
-
-                try self.dylibs.append(self.allocator, dylib);
-            },
-        }
-    }
 }
 
 fn parseLibs(self: *Zld, libs: []const []const u8) !void {
-    for (libs) |lib| {
-        const file = try fs.cwd().openFile(lib, .{});
-
-        var kind: ?union(enum) {
-            archive,
-            dylib,
-            stub: LibStub,
-        } = kind: {
-            if (try Archive.isArchive(file)) break :kind .archive;
-            if (try Dylib.isDylib(file)) break :kind .dylib;
-            var lib_stub = LibStub.loadFromFile(self.allocator, file) catch {
-                break :kind null;
-            };
-            break :kind .{ .stub = lib_stub };
-        };
-        defer {
-            if (kind) |*kk| {
-                switch (kk.*) {
-                    .stub => |*stub| {
-                        stub.deinit();
-                    },
-                    else => {},
-                }
+    const DylibDeps = struct {
+        fn bubbleUp(out: *std.ArrayList(*Dylib), next: *Dylib) error{OutOfMemory}!void {
+            try out.ensureUnusedCapacity(next.dylibs.items.len);
+            for (next.dylibs.items) |dylib| {
+                out.appendAssumeCapacity(dylib);
+            }
+            for (next.dylibs.items) |dylib| {
+                try bubbleUp(out, dylib);
             }
         }
+    };
 
-        const unwrapped = kind orelse {
-            file.close();
-            log.warn("unknown filetype for a library: '{s}'", .{lib});
+    for (libs) |lib| {
+        if (try Dylib.createAndParseFromPath(
+            self.allocator,
+            self.arch.?,
+            lib,
+            self.syslibroot,
+            true,
+        )) |dylib| {
+            try self.dylibs.append(self.allocator, dylib);
             continue;
-        };
-        switch (unwrapped) {
-            .archive => {
-                const archive = try self.allocator.create(Archive);
-                errdefer self.allocator.destroy(archive);
-
-                archive.* = Archive.init(self.allocator);
-                archive.arch = self.arch.?;
-                archive.name = try self.allocator.dupe(u8, lib);
-                archive.file = file;
-
-                try archive.parse();
-                try self.archives.append(self.allocator, archive);
-            },
-            .dylib, .stub => {
-                const dylib = try self.allocator.create(Dylib);
-                errdefer self.allocator.destroy(dylib);
-
-                dylib.* = Dylib.init(self.allocator);
-                dylib.arch = self.arch.?;
-                dylib.name = try self.allocator.dupe(u8, lib);
-
-                if (unwrapped == .dylib) {
-                    dylib.file = file;
-                    try dylib.parse();
-                } else {
-                    try dylib.parseFromStub(unwrapped.stub);
-                }
-
-                try self.dylibs.append(self.allocator, dylib);
-            },
         }
-    }
-}
 
-fn parseLibSystem(self: *Zld, libc_stub_path: []const u8) !void {
-    const file = try fs.cwd().openFile(libc_stub_path, .{});
-    defer file.close();
+        if (try Archive.createAndParseFromPath(self.allocator, self.arch.?, lib)) |archive| {
+            try self.archives.append(self.allocator, archive);
+            continue;
+        }
 
-    var lib_stub = try LibStub.loadFromFile(self.allocator, file);
-    defer lib_stub.deinit();
+        log.warn("unknown filetype for a library: '{s}'", .{lib});
+    }
 
-    const dylib = try self.allocator.create(Dylib);
-    errdefer self.allocator.destroy(dylib);
+    // Flatten out any parsed dependencies.
+    var deps = std.ArrayList(*Dylib).init(self.allocator);
+    defer deps.deinit();
 
-    dylib.* = Dylib.init(self.allocator);
-    dylib.arch = self.arch.?;
-    dylib.name = try self.allocator.dupe(u8, libc_stub_path);
+    for (self.dylibs.items) |dylib| {
+        try DylibDeps.bubbleUp(&deps, dylib);
+    }
 
-    try dylib.parseFromStub(lib_stub);
+    try self.dylibs.appendSlice(self.allocator, deps.toOwnedSlice());
+}
 
+fn parseLibSystem(self: *Zld, libc_stub_path: []const u8) !void {
+    const dylib = (try Dylib.createAndParseFromPath(
+        self.allocator,
+        self.arch.?,
+        libc_stub_path,
+        self.syslibroot,
+        false,
+    )) orelse return error.FailedToParseLibSystem;
     self.libsystem_dylib_index = @intCast(u16, self.dylibs.items.len);
     try self.dylibs.append(self.allocator, dylib);
 
src/link/MachO.zig
@@ -789,6 +789,7 @@ fn linkWithLLD(self: *MachO, comp: *Compilation) !void {
                 zld.deinit();
             }
             zld.arch = target.cpu.arch;
+            zld.syslibroot = self.base.options.syslibroot;
             zld.stack_size = stack_size;
 
             // Positional arguments to the linker such as object files and static archives.
src/link/tapi.zig
@@ -41,7 +41,8 @@ pub const LibStub = struct {
         },
         reexports: ?[]const struct {
             targets: []const []const u8,
-            symbols: []const []const u8,
+            symbols: ?[]const []const u8,
+            objc_classes: ?[]const []const u8,
         },
         allowable_clients: ?[]const struct {
             targets: []const []const u8,