1/// Non-zero for fat dylibs
2offset: u64,
3path: Path,
4index: File.Index,
5file_handle: File.HandleIndex,
6tag: enum { dylib, tbd },
7
8exports: std.MultiArrayList(Export) = .{},
9strtab: std.ArrayList(u8) = .empty,
10id: ?Id = null,
11ordinal: u16 = 0,
12
13symbols: std.ArrayList(Symbol) = .empty,
14symbols_extra: std.ArrayList(u32) = .empty,
15globals: std.ArrayList(MachO.SymbolResolver.Index) = .empty,
16dependents: std.ArrayList(Id) = .empty,
17rpaths: std.array_hash_map.String(void) = .empty,
18umbrella: File.Index,
19platform: ?MachO.Platform = null,
20
21needed: bool,
22weak: bool,
23reexport: bool,
24explicit: bool,
25hoisted: bool = true,
26referenced: bool = false,
27
28output_symtab_ctx: MachO.SymtabCtx = .{},
29
30pub fn deinit(self: *Dylib, allocator: Allocator) void {
31 allocator.free(self.path.sub_path);
32 self.exports.deinit(allocator);
33 self.strtab.deinit(allocator);
34 if (self.id) |*id| id.deinit(allocator);
35 self.symbols.deinit(allocator);
36 self.symbols_extra.deinit(allocator);
37 self.globals.deinit(allocator);
38 for (self.dependents.items) |*id| {
39 id.deinit(allocator);
40 }
41 self.dependents.deinit(allocator);
42 for (self.rpaths.keys()) |rpath| {
43 allocator.free(rpath);
44 }
45 self.rpaths.deinit(allocator);
46}
47
48pub fn parse(self: *Dylib, macho_file: *MachO) !void {
49 switch (self.tag) {
50 .tbd => try self.parseTbd(macho_file),
51 .dylib => try self.parseBinary(macho_file),
52 }
53 try self.initSymbols(macho_file);
54}
55
56fn parseBinary(self: *Dylib, macho_file: *MachO) !void {
57 const tracy = trace(@src());
58 defer tracy.end();
59
60 const comp = macho_file.base.comp;
61 const io = comp.io;
62 const gpa = comp.gpa;
63 const file = macho_file.getFileHandle(self.file_handle);
64 const offset = self.offset;
65
66 log.debug("parsing dylib from binary: {f}", .{@as(Path, self.path)});
67
68 var header_buffer: [@sizeOf(macho.mach_header_64)]u8 = undefined;
69 {
70 const amt = try file.readPositionalAll(io, &header_buffer, offset);
71 if (amt != @sizeOf(macho.mach_header_64)) return error.InputOutput;
72 }
73 const header = @as(*align(1) const macho.mach_header_64, @ptrCast(&header_buffer)).*;
74
75 const this_cpu_arch: std.Target.Cpu.Arch = switch (header.cputype) {
76 macho.CPU_TYPE_ARM64 => .aarch64,
77 macho.CPU_TYPE_X86_64 => .x86_64,
78 else => |x| {
79 try macho_file.reportParseError2(self.index, "unknown cpu architecture: {d}", .{x});
80 return error.InvalidMachineType;
81 },
82 };
83 if (macho_file.getTarget().cpu.arch != this_cpu_arch) {
84 try macho_file.reportParseError2(self.index, "invalid cpu architecture: {s}", .{@tagName(this_cpu_arch)});
85 return error.InvalidMachineType;
86 }
87
88 const lc_buffer = try gpa.alloc(u8, header.sizeofcmds);
89 defer gpa.free(lc_buffer);
90 {
91 const amt = try file.readPositionalAll(io, lc_buffer, offset + @sizeOf(macho.mach_header_64));
92 if (amt != lc_buffer.len) return error.InputOutput;
93 }
94
95 var it = LoadCommandIterator.init(&header, lc_buffer) catch |err| std.debug.panic("bad dylib: {t}", .{err});
96 while (it.next() catch |err| std.debug.panic("bad dylib: {t}", .{err})) |cmd| switch (cmd.hdr.cmd) {
97 .ID_DYLIB => {
98 self.id = try Id.fromLoadCommand(gpa, cmd.cast(macho.dylib_command).?, cmd.getDylibPathName());
99 },
100 .REEXPORT_DYLIB => if (header.flags & macho.MH_NO_REEXPORTED_DYLIBS == 0) {
101 const id = try Id.fromLoadCommand(gpa, cmd.cast(macho.dylib_command).?, cmd.getDylibPathName());
102 try self.dependents.append(gpa, id);
103 },
104 .DYLD_INFO_ONLY => {
105 const dyld_cmd = cmd.cast(macho.dyld_info_command).?;
106 const data = try gpa.alloc(u8, dyld_cmd.export_size);
107 defer gpa.free(data);
108 const amt = try file.readPositionalAll(io, data, dyld_cmd.export_off + offset);
109 if (amt != data.len) return error.InputOutput;
110 try self.parseTrie(data, macho_file);
111 },
112 .DYLD_EXPORTS_TRIE => {
113 const ld_cmd = cmd.cast(macho.linkedit_data_command).?;
114 const data = try gpa.alloc(u8, ld_cmd.datasize);
115 defer gpa.free(data);
116 const amt = try file.readPositionalAll(io, data, ld_cmd.dataoff + offset);
117 if (amt != data.len) return error.InputOutput;
118 try self.parseTrie(data, macho_file);
119 },
120 .RPATH => {
121 const path = cmd.getRpathPathName();
122 try self.rpaths.put(gpa, try gpa.dupe(u8, path), {});
123 },
124 .BUILD_VERSION,
125 .VERSION_MIN_MACOSX,
126 .VERSION_MIN_IPHONEOS,
127 .VERSION_MIN_TVOS,
128 .VERSION_MIN_WATCHOS,
129 => {
130 self.platform = MachO.Platform.fromLoadCommand(cmd);
131 },
132 else => {},
133 };
134
135 if (self.id == null) {
136 try macho_file.reportParseError2(self.index, "missing LC_ID_DYLIB load command", .{});
137 return error.MalformedDylib;
138 }
139
140 if (self.platform) |platform| {
141 if (!macho_file.platform.eqlTarget(platform)) {
142 try macho_file.reportParseError2(self.index, "invalid platform: {f}", .{
143 platform.fmtTarget(macho_file.getTarget().cpu.arch),
144 });
145 return error.InvalidTarget;
146 }
147 // TODO: this can cause the CI to fail so I'm commenting this check out so that
148 // I can work out the rest of the changes first
149 // if (macho_file.platform.version.order(platform.version) == .lt) {
150 // try macho_file.reportParseError2(self.index, "object file built for newer platform: {f}: {f} < {f}", .{
151 // macho_file.platform.fmtTarget(macho_file.getTarget().cpu.arch),
152 // macho_file.platform.version,
153 // platform.version,
154 // });
155 // return error.InvalidTarget;
156 // }
157 }
158}
159
160const TrieIterator = struct {
161 stream: std.Io.Reader,
162
163 fn readUleb128(it: *TrieIterator) !u64 {
164 return it.stream.takeLeb128(u64);
165 }
166
167 fn readString(it: *TrieIterator) ![:0]const u8 {
168 return it.stream.takeSentinel(0);
169 }
170
171 fn readByte(it: *TrieIterator) !u8 {
172 return it.stream.takeByte();
173 }
174};
175
176pub fn addExport(self: *Dylib, allocator: Allocator, name: []const u8, flags: Export.Flags) !void {
177 try self.exports.append(allocator, .{
178 .name = try self.addString(allocator, name),
179 .flags = flags,
180 });
181}
182
183fn parseTrieNode(
184 self: *Dylib,
185 it: *TrieIterator,
186 allocator: Allocator,
187 arena: Allocator,
188 prefix: []const u8,
189) !void {
190 const tracy = trace(@src());
191 defer tracy.end();
192 const size = try it.readUleb128();
193 if (size > 0) {
194 const flags = try it.readUleb128();
195 const kind = flags & macho.EXPORT_SYMBOL_FLAGS_KIND_MASK;
196 const out_flags = Export.Flags{
197 .abs = kind == macho.EXPORT_SYMBOL_FLAGS_KIND_ABSOLUTE,
198 .tlv = kind == macho.EXPORT_SYMBOL_FLAGS_KIND_THREAD_LOCAL,
199 .weak = flags & macho.EXPORT_SYMBOL_FLAGS_WEAK_DEFINITION != 0,
200 };
201 if (flags & macho.EXPORT_SYMBOL_FLAGS_REEXPORT != 0) {
202 _ = try it.readUleb128(); // dylib ordinal
203 const name = try it.readString();
204 try self.addExport(allocator, if (name.len > 0) name else prefix, out_flags);
205 } else if (flags & macho.EXPORT_SYMBOL_FLAGS_STUB_AND_RESOLVER != 0) {
206 _ = try it.readUleb128(); // stub offset
207 _ = try it.readUleb128(); // resolver offset
208 try self.addExport(allocator, prefix, out_flags);
209 } else {
210 _ = try it.readUleb128(); // VM offset
211 try self.addExport(allocator, prefix, out_flags);
212 }
213 }
214
215 const nedges = try it.readByte();
216
217 for (0..nedges) |_| {
218 const label = try it.readString();
219 const off = try it.readUleb128();
220 const prefix_label = try std.fmt.allocPrint(arena, "{s}{s}", .{ prefix, label });
221 const curr = it.stream.seek;
222 it.stream.seek = math.cast(usize, off) orelse return error.Overflow;
223 try self.parseTrieNode(it, allocator, arena, prefix_label);
224 it.stream.seek = curr;
225 }
226}
227
228fn parseTrie(self: *Dylib, data: []const u8, macho_file: *MachO) !void {
229 const tracy = trace(@src());
230 defer tracy.end();
231 const gpa = macho_file.base.comp.gpa;
232 var arena = std.heap.ArenaAllocator.init(gpa);
233 defer arena.deinit();
234
235 var it: TrieIterator = .{ .stream = .fixed(data) };
236 try self.parseTrieNode(&it, gpa, arena.allocator(), "");
237}
238
239fn parseTbd(self: *Dylib, macho_file: *MachO) !void {
240 const tracy = trace(@src());
241 defer tracy.end();
242
243 const comp = macho_file.base.comp;
244 const gpa = comp.gpa;
245 const io = comp.io;
246
247 log.debug("parsing dylib from stub: {f}", .{self.path});
248
249 const file = macho_file.getFileHandle(self.file_handle);
250 var lib_stub = LibStub.loadFromFile(gpa, io, file) catch |err| {
251 try macho_file.reportParseError2(self.index, "failed to parse TBD file: {t}", .{err});
252 return error.MalformedTbd;
253 };
254 defer lib_stub.deinit();
255 const umbrella_lib = lib_stub.inner[0];
256
257 {
258 var id = try Id.default(gpa, umbrella_lib.installName());
259 if (umbrella_lib.currentVersion()) |version| {
260 try id.parseCurrentVersion(version);
261 }
262 if (umbrella_lib.compatibilityVersion()) |version| {
263 try id.parseCompatibilityVersion(version);
264 }
265 self.id = id;
266 }
267
268 var umbrella_libs = std.StringHashMap(void).init(gpa);
269 defer umbrella_libs.deinit();
270
271 log.debug(" (install_name '{s}')", .{umbrella_lib.installName()});
272
273 const cpu_arch = macho_file.getTarget().cpu.arch;
274 self.platform = macho_file.platform;
275
276 var matcher = try TargetMatcher.init(gpa, cpu_arch, self.platform.?.toApplePlatform());
277 defer matcher.deinit();
278
279 for (lib_stub.inner, 0..) |elem, stub_index| {
280 if (!(try matcher.matchesTargetTbd(elem))) continue;
281
282 if (stub_index > 0) {
283 // TODO I thought that we could switch on presence of `parent-umbrella` map;
284 // however, turns out `libsystem_notify.dylib` is fully reexported by `libSystem.dylib`
285 // BUT does not feature a `parent-umbrella` map as the only sublib. Apple's bug perhaps?
286 try umbrella_libs.put(elem.installName(), {});
287 }
288
289 switch (elem) {
290 .v3 => |stub| {
291 if (stub.exports) |exports| {
292 for (exports) |exp| {
293 if (!matcher.matchesArch(exp.archs)) continue;
294
295 if (exp.symbols) |symbols| {
296 for (symbols) |sym_name| {
297 try self.addExport(gpa, sym_name, .{});
298 }
299 }
300
301 if (exp.weak_symbols) |symbols| {
302 for (symbols) |sym_name| {
303 try self.addExport(gpa, sym_name, .{ .weak = true });
304 }
305 }
306
307 if (exp.objc_classes) |objc_classes| {
308 for (objc_classes) |class_name| {
309 try self.addObjCClass(gpa, class_name);
310 }
311 }
312
313 if (exp.objc_ivars) |objc_ivars| {
314 for (objc_ivars) |ivar| {
315 try self.addObjCIVar(gpa, ivar);
316 }
317 }
318
319 if (exp.objc_eh_types) |objc_eh_types| {
320 for (objc_eh_types) |eht| {
321 try self.addObjCEhType(gpa, eht);
322 }
323 }
324
325 if (exp.re_exports) |re_exports| {
326 for (re_exports) |lib| {
327 if (umbrella_libs.contains(lib)) continue;
328
329 log.debug(" (found re-export '{s}')", .{lib});
330
331 const dep_id = try Id.default(gpa, lib);
332 try self.dependents.append(gpa, dep_id);
333 }
334 }
335 }
336 }
337 },
338 .v4 => |stub| {
339 if (stub.exports) |exports| {
340 for (exports) |exp| {
341 if (!matcher.matchesTarget(exp.targets)) continue;
342
343 if (exp.symbols) |symbols| {
344 for (symbols) |sym_name| {
345 try self.addExport(gpa, sym_name, .{});
346 }
347 }
348
349 if (exp.weak_symbols) |symbols| {
350 for (symbols) |sym_name| {
351 try self.addExport(gpa, sym_name, .{ .weak = true });
352 }
353 }
354
355 if (exp.objc_classes) |classes| {
356 for (classes) |sym_name| {
357 try self.addObjCClass(gpa, sym_name);
358 }
359 }
360
361 if (exp.objc_ivars) |objc_ivars| {
362 for (objc_ivars) |ivar| {
363 try self.addObjCIVar(gpa, ivar);
364 }
365 }
366
367 if (exp.objc_eh_types) |objc_eh_types| {
368 for (objc_eh_types) |eht| {
369 try self.addObjCEhType(gpa, eht);
370 }
371 }
372 }
373 }
374
375 if (stub.reexports) |reexports| {
376 for (reexports) |reexp| {
377 if (!matcher.matchesTarget(reexp.targets)) continue;
378
379 if (reexp.symbols) |symbols| {
380 for (symbols) |sym_name| {
381 try self.addExport(gpa, sym_name, .{});
382 }
383 }
384
385 if (reexp.weak_symbols) |symbols| {
386 for (symbols) |sym_name| {
387 try self.addExport(gpa, sym_name, .{ .weak = true });
388 }
389 }
390
391 if (reexp.objc_classes) |classes| {
392 for (classes) |sym_name| {
393 try self.addObjCClass(gpa, sym_name);
394 }
395 }
396
397 if (reexp.objc_ivars) |objc_ivars| {
398 for (objc_ivars) |ivar| {
399 try self.addObjCIVar(gpa, ivar);
400 }
401 }
402
403 if (reexp.objc_eh_types) |objc_eh_types| {
404 for (objc_eh_types) |eht| {
405 try self.addObjCEhType(gpa, eht);
406 }
407 }
408 }
409 }
410
411 if (stub.objc_classes) |classes| {
412 for (classes) |sym_name| {
413 try self.addObjCClass(gpa, sym_name);
414 }
415 }
416
417 if (stub.objc_ivars) |objc_ivars| {
418 for (objc_ivars) |ivar| {
419 try self.addObjCIVar(gpa, ivar);
420 }
421 }
422
423 if (stub.objc_eh_types) |objc_eh_types| {
424 for (objc_eh_types) |eht| {
425 try self.addObjCEhType(gpa, eht);
426 }
427 }
428 },
429 }
430 }
431
432 // For V4, we add dependent libs in a separate pass since some stubs such as libSystem include
433 // re-exports directly in the stub file.
434 for (lib_stub.inner) |elem| {
435 if (elem == .v3) continue;
436 const stub = elem.v4;
437
438 if (stub.reexported_libraries) |reexports| {
439 for (reexports) |reexp| {
440 if (!matcher.matchesTarget(reexp.targets)) continue;
441
442 for (reexp.libraries) |lib| {
443 if (umbrella_libs.contains(lib)) continue;
444
445 log.debug(" (found re-export '{s}')", .{lib});
446
447 const dep_id = try Id.default(gpa, lib);
448 try self.dependents.append(gpa, dep_id);
449 }
450 }
451 }
452 }
453}
454
455fn addObjCClass(self: *Dylib, allocator: Allocator, name: []const u8) !void {
456 try self.addObjCExport(allocator, "_OBJC_CLASS_", name);
457 try self.addObjCExport(allocator, "_OBJC_METACLASS_", name);
458}
459
460fn addObjCIVar(self: *Dylib, allocator: Allocator, name: []const u8) !void {
461 try self.addObjCExport(allocator, "_OBJC_IVAR_", name);
462}
463
464fn addObjCEhType(self: *Dylib, allocator: Allocator, name: []const u8) !void {
465 try self.addObjCExport(allocator, "_OBJC_EHTYPE_", name);
466}
467
468fn addObjCExport(
469 self: *Dylib,
470 allocator: Allocator,
471 comptime prefix: []const u8,
472 name: []const u8,
473) !void {
474 const full_name = try std.fmt.allocPrint(allocator, prefix ++ "$_{s}", .{name});
475 defer allocator.free(full_name);
476 try self.addExport(allocator, full_name, .{});
477}
478
479fn initSymbols(self: *Dylib, macho_file: *MachO) !void {
480 const gpa = macho_file.base.comp.gpa;
481
482 const nsyms = self.exports.items(.name).len;
483 try self.symbols.ensureTotalCapacityPrecise(gpa, nsyms);
484 try self.symbols_extra.ensureTotalCapacityPrecise(gpa, nsyms * @sizeOf(Symbol.Extra));
485 try self.globals.ensureTotalCapacityPrecise(gpa, nsyms);
486 self.globals.resize(gpa, nsyms) catch unreachable;
487 @memset(self.globals.items, 0);
488
489 for (self.exports.items(.name), self.exports.items(.flags)) |noff, flags| {
490 const index = self.addSymbolAssumeCapacity();
491 const symbol = &self.symbols.items[index];
492 symbol.name = noff;
493 symbol.extra = self.addSymbolExtraAssumeCapacity(.{});
494 symbol.flags.weak = flags.weak;
495 symbol.flags.tlv = flags.tlv;
496 symbol.visibility = .global;
497 }
498}
499
500pub fn resolveSymbols(self: *Dylib, macho_file: *MachO) !void {
501 const tracy = trace(@src());
502 defer tracy.end();
503
504 if (!self.explicit and !self.hoisted) return;
505
506 const gpa = macho_file.base.comp.gpa;
507
508 for (self.exports.items(.flags), self.globals.items, 0..) |flags, *global, i| {
509 const gop = try macho_file.resolver.getOrPut(gpa, .{
510 .index = @intCast(i),
511 .file = self.index,
512 }, macho_file);
513 if (!gop.found_existing) {
514 gop.ref.* = .{ .index = 0, .file = 0 };
515 }
516 global.* = gop.index;
517
518 if (gop.ref.getFile(macho_file) == null) {
519 gop.ref.* = .{ .index = @intCast(i), .file = self.index };
520 continue;
521 }
522
523 if (self.asFile().getSymbolRank(.{
524 .weak = flags.weak,
525 }) < gop.ref.getSymbol(macho_file).?.getSymbolRank(macho_file)) {
526 gop.ref.* = .{ .index = @intCast(i), .file = self.index };
527 }
528 }
529}
530
531pub fn isAlive(self: Dylib, macho_file: *MachO) bool {
532 if (!macho_file.dead_strip_dylibs) return self.explicit or self.referenced or self.needed;
533 return self.referenced or self.needed;
534}
535
536pub fn markReferenced(self: *Dylib, macho_file: *MachO) void {
537 const tracy = trace(@src());
538 defer tracy.end();
539
540 for (0..self.symbols.items.len) |i| {
541 const ref = self.getSymbolRef(@intCast(i), macho_file);
542 const file = ref.getFile(macho_file) orelse continue;
543 if (file.getIndex() != self.index) continue;
544 const global = ref.getSymbol(macho_file).?;
545 if (global.isLocal()) continue;
546 self.referenced = true;
547 break;
548 }
549}
550
551pub fn calcSymtabSize(self: *Dylib, macho_file: *MachO) void {
552 const tracy = trace(@src());
553 defer tracy.end();
554
555 for (self.symbols.items, 0..) |*sym, i| {
556 const ref = self.getSymbolRef(@intCast(i), macho_file);
557 const file = ref.getFile(macho_file) orelse continue;
558 if (file.getIndex() != self.index) continue;
559 if (sym.isLocal()) continue;
560 assert(sym.flags.import);
561 sym.flags.output_symtab = true;
562 sym.addExtra(.{ .symtab = self.output_symtab_ctx.nimports }, macho_file);
563 self.output_symtab_ctx.nimports += 1;
564 self.output_symtab_ctx.strsize += @as(u32, @intCast(sym.getName(macho_file).len + 1));
565 }
566}
567
568pub fn writeSymtab(self: Dylib, macho_file: *MachO, ctx: anytype) void {
569 const tracy = trace(@src());
570 defer tracy.end();
571
572 var n_strx = self.output_symtab_ctx.stroff;
573 for (self.symbols.items, 0..) |sym, i| {
574 const ref = self.getSymbolRef(@intCast(i), macho_file);
575 const file = ref.getFile(macho_file) orelse continue;
576 if (file.getIndex() != self.index) continue;
577 const idx = sym.getOutputSymtabIndex(macho_file) orelse continue;
578 const out_sym = &ctx.symtab.items[idx];
579 out_sym.n_strx = n_strx;
580 sym.setOutputSym(macho_file, out_sym);
581 const name = sym.getName(macho_file);
582 @memcpy(ctx.strtab.items[n_strx..][0..name.len], name);
583 n_strx += @intCast(name.len);
584 ctx.strtab.items[n_strx] = 0;
585 n_strx += 1;
586 }
587}
588
589pub inline fn getUmbrella(self: Dylib, macho_file: *MachO) *Dylib {
590 return macho_file.getFile(self.umbrella).?.dylib;
591}
592
593fn addString(self: *Dylib, allocator: Allocator, name: []const u8) !MachO.String {
594 const off = @as(u32, @intCast(self.strtab.items.len));
595 try self.strtab.ensureUnusedCapacity(allocator, name.len + 1);
596 self.strtab.appendSliceAssumeCapacity(name);
597 self.strtab.appendAssumeCapacity(0);
598 return .{ .pos = off, .len = @intCast(name.len + 1) };
599}
600
601pub fn getString(self: Dylib, string: MachO.String) [:0]const u8 {
602 assert(string.pos < self.strtab.items.len and string.pos + string.len <= self.strtab.items.len);
603 if (string.len == 0) return "";
604 return self.strtab.items[string.pos..][0 .. string.len - 1 :0];
605}
606
607pub fn asFile(self: *Dylib) File {
608 return .{ .dylib = self };
609}
610
611fn addSymbol(self: *Dylib, allocator: Allocator) !Symbol.Index {
612 try self.symbols.ensureUnusedCapacity(allocator, 1);
613 return self.addSymbolAssumeCapacity();
614}
615
616fn addSymbolAssumeCapacity(self: *Dylib) Symbol.Index {
617 const index: Symbol.Index = @intCast(self.symbols.items.len);
618 const symbol = self.symbols.addOneAssumeCapacity();
619 symbol.* = .{ .file = self.index };
620 return index;
621}
622
623pub fn getSymbolRef(self: Dylib, index: Symbol.Index, macho_file: *MachO) MachO.Ref {
624 const global_index = self.globals.items[index];
625 if (macho_file.resolver.get(global_index)) |ref| return ref;
626 return .{ .index = index, .file = self.index };
627}
628
629pub fn addSymbolExtra(self: *Dylib, allocator: Allocator, extra: Symbol.Extra) !u32 {
630 const field_count = @typeInfo(Symbol.Extra).@"struct".field_names.len;
631 try self.symbols_extra.ensureUnusedCapacity(allocator, field_count);
632 return self.addSymbolExtraAssumeCapacity(extra);
633}
634
635fn addSymbolExtraAssumeCapacity(self: *Dylib, extra: Symbol.Extra) u32 {
636 const index = @as(u32, @intCast(self.symbols_extra.items.len));
637 const info = @typeInfo(Symbol.Extra).@"struct";
638 inline for (info.field_names, info.field_types) |field_name, field_type| {
639 self.symbols_extra.appendAssumeCapacity(switch (field_type) {
640 u32 => @field(extra, field_name),
641 else => @compileError("bad field type"),
642 });
643 }
644 return index;
645}
646
647pub fn getSymbolExtra(self: Dylib, index: u32) Symbol.Extra {
648 const info = @typeInfo(Symbol.Extra).@"struct";
649 var i: usize = index;
650 var result: Symbol.Extra = undefined;
651 inline for (info.field_names, info.field_types) |field_name, field_type| {
652 @field(result, field_name) = switch (field_type) {
653 u32 => self.symbols_extra.items[i],
654 else => @compileError("bad field type"),
655 };
656 i += 1;
657 }
658 return result;
659}
660
661pub fn setSymbolExtra(self: *Dylib, index: u32, extra: Symbol.Extra) void {
662 const info = @typeInfo(Symbol.Extra).@"struct";
663 inline for (info.field_names, info.field_types, 0..) |field_name, field_type, i| {
664 self.symbols_extra.items[index + i] = switch (field_type) {
665 u32 => @field(extra, field_name),
666 else => @compileError("bad field type"),
667 };
668 }
669}
670
671pub fn fmtSymtab(self: *Dylib, macho_file: *MachO) std.fmt.Alt(Format, Format.symtab) {
672 return .{ .data = .{
673 .dylib = self,
674 .macho_file = macho_file,
675 } };
676}
677
678const Format = struct {
679 dylib: *Dylib,
680 macho_file: *MachO,
681
682 fn symtab(f: Format, w: *Writer) Writer.Error!void {
683 const dylib = f.dylib;
684 const macho_file = f.macho_file;
685 try w.writeAll(" globals\n");
686 for (dylib.symbols.items, 0..) |sym, i| {
687 const ref = dylib.getSymbolRef(@intCast(i), macho_file);
688 if (ref.getFile(macho_file) == null) {
689 // TODO any better way of handling this?
690 try w.print(" {s} : unclaimed\n", .{sym.getName(macho_file)});
691 } else {
692 try w.print(" {f}\n", .{ref.getSymbol(macho_file).?.fmt(macho_file)});
693 }
694 }
695 }
696};
697
698pub const TargetMatcher = struct {
699 allocator: Allocator,
700 cpu_arch: std.Target.Cpu.Arch,
701 platform: macho.PLATFORM,
702 target_strings: std.ArrayList([]const u8) = .empty,
703
704 pub fn init(allocator: Allocator, cpu_arch: std.Target.Cpu.Arch, platform: macho.PLATFORM) !TargetMatcher {
705 var self = TargetMatcher{
706 .allocator = allocator,
707 .cpu_arch = cpu_arch,
708 .platform = platform,
709 };
710
711 try self.addTargetStrings(cpuArchToAppleString(cpu_arch));
712 // In Xcode 26.4, Apple unified their TBD files from having separate `arm64-macos` and `arm64e-macos`
713 // entries to having just the latter, presumably because the symbol lists are identical anyway. It
714 // sure would have been nice if they settled on the former as the unified name so as not to break the
715 // world, but evidently we can't have nice things.
716 if (cpu_arch == .aarch64) try self.addTargetStrings("arm64e");
717
718 return self;
719 }
720
721 fn addTargetStrings(self: *TargetMatcher, arch: []const u8) !void {
722 try self.target_strings.append(self.allocator, try std.fmt.allocPrint(
723 self.allocator,
724 "{s}-{s}",
725 .{ arch, platformToAppleString(self.platform) },
726 ));
727
728 switch (self.platform) {
729 .MACCATALYST => {
730 // Mac Catalyst is allowed to link macOS libraries in a TBD because Apple were apparently too lazy
731 // to add the proper target strings despite doing so in other places in the format???
732 try self.target_strings.append(self.allocator, try std.fmt.allocPrint(self.allocator, "{s}-macos", .{arch}));
733 },
734 .IOSSIMULATOR, .TVOSSIMULATOR, .WATCHOSSIMULATOR, .VISIONOSSIMULATOR => {
735 // For Apple simulator targets, we need to link against the simulator host's libraries too.
736 try self.target_strings.append(self.allocator, try std.fmt.allocPrint(self.allocator, "{s}-macos", .{arch}));
737 },
738 else => {},
739 }
740 }
741
742 pub fn deinit(self: *TargetMatcher) void {
743 for (self.target_strings.items) |t| {
744 self.allocator.free(t);
745 }
746 self.target_strings.deinit(self.allocator);
747 }
748
749 fn cpuArchToAppleString(cpu_arch: std.Target.Cpu.Arch) []const u8 {
750 return switch (cpu_arch) {
751 .aarch64 => "arm64",
752 .x86_64 => "x86_64",
753 else => unreachable,
754 };
755 }
756
757 fn platformToAppleString(platform: macho.PLATFORM) []const u8 {
758 return switch (platform) {
759 .MACOS => "macos",
760 .IOS => "ios",
761 .TVOS => "tvos",
762 .WATCHOS => "watchos",
763 .VISIONOS => "xros",
764 .IOSSIMULATOR => "ios-simulator",
765 .TVOSSIMULATOR => "tvos-simulator",
766 .WATCHOSSIMULATOR => "watchos-simulator",
767 .VISIONOSSIMULATOR => "xros-simulator",
768 .BRIDGEOS => "bridgeos",
769 .MACCATALYST => "maccatalyst",
770 .DRIVERKIT => "driverkit",
771 else => unreachable,
772 };
773 }
774
775 fn hasValue(stack: []const []const u8, needle: []const u8) bool {
776 for (stack) |v| {
777 if (mem.eql(u8, v, needle)) return true;
778 }
779 return false;
780 }
781
782 fn matchesArch(self: TargetMatcher, archs: []const []const u8) bool {
783 return hasValue(archs, cpuArchToAppleString(self.cpu_arch));
784 }
785
786 fn matchesTarget(self: TargetMatcher, targets: []const []const u8) bool {
787 for (self.target_strings.items) |t| {
788 if (hasValue(targets, t)) return true;
789 }
790 return false;
791 }
792
793 pub fn matchesTargetTbd(self: TargetMatcher, tbd: Tbd) !bool {
794 var arena = std.heap.ArenaAllocator.init(self.allocator);
795 defer arena.deinit();
796
797 const targets = switch (tbd) {
798 .v3 => |v3| blk: {
799 var targets = std.array_list.Managed([]const u8).init(arena.allocator());
800 for (v3.archs) |arch| {
801 if (mem.eql(u8, v3.platform, "zippered")) {
802 // From Xcode 10.3 → 11.3.1, macos SDK .tbd files specify platform as 'zippered'
803 // which should map to [ '<arch>-macos', '<arch>-maccatalyst' ]
804 try targets.append(try std.fmt.allocPrint(arena.allocator(), "{s}-macos", .{arch}));
805 try targets.append(try std.fmt.allocPrint(arena.allocator(), "{s}-maccatalyst", .{arch}));
806 } else {
807 try targets.append(try std.fmt.allocPrint(arena.allocator(), "{s}-{s}", .{ arch, v3.platform }));
808 }
809 }
810 break :blk targets.items;
811 },
812 .v4 => |v4| v4.targets,
813 };
814
815 return self.matchesTarget(targets);
816 }
817};
818
819pub const Id = struct {
820 name: []const u8,
821 timestamp: u32,
822 current_version: u32,
823 compatibility_version: u32,
824
825 pub fn default(allocator: Allocator, name: []const u8) !Id {
826 return Id{
827 .name = try allocator.dupe(u8, name),
828 .timestamp = 2,
829 .current_version = 0x10000,
830 .compatibility_version = 0x10000,
831 };
832 }
833
834 pub fn fromLoadCommand(allocator: Allocator, lc: macho.dylib_command, name: []const u8) !Id {
835 return Id{
836 .name = try allocator.dupe(u8, name),
837 .timestamp = lc.dylib.timestamp,
838 .current_version = lc.dylib.current_version,
839 .compatibility_version = lc.dylib.compatibility_version,
840 };
841 }
842
843 pub fn deinit(id: Id, allocator: Allocator) void {
844 allocator.free(id.name);
845 }
846
847 pub const ParseError = fmt.ParseIntError || mem.PrintError;
848
849 pub fn parseCurrentVersion(id: *Id, version: anytype) ParseError!void {
850 id.current_version = try parseVersion(version);
851 }
852
853 pub fn parseCompatibilityVersion(id: *Id, version: anytype) ParseError!void {
854 id.compatibility_version = try parseVersion(version);
855 }
856
857 fn parseVersion(version: anytype) ParseError!u32 {
858 const string = blk: {
859 switch (version) {
860 .int => |int| {
861 var out: u32 = 0;
862 const major = math.cast(u16, int) orelse return error.Overflow;
863 out += @as(u32, @intCast(major)) << 16;
864 return out;
865 },
866 .float => |float| {
867 var buf: [256]u8 = undefined;
868 break :blk try mem.print(&buf, "{d}", .{float});
869 },
870 .string => |string| {
871 break :blk string;
872 },
873 }
874 };
875
876 var out: u32 = 0;
877 var values: [3][]const u8 = undefined;
878
879 var split = mem.splitScalar(u8, string, '.');
880 var count: u4 = 0;
881 while (split.next()) |value| {
882 if (count > 2) {
883 log.debug("malformed version field: {s}", .{string});
884 return 0x10000;
885 }
886 values[count] = value;
887 count += 1;
888 }
889
890 if (count > 2) {
891 out += try fmt.parseInt(u8, values[2], 10);
892 }
893 if (count > 1) {
894 out += @as(u32, @intCast(try fmt.parseInt(u8, values[1], 10))) << 8;
895 }
896 out += @as(u32, @intCast(try fmt.parseInt(u16, values[0], 10))) << 16;
897
898 return out;
899 }
900};
901
902const Export = struct {
903 name: MachO.String,
904 flags: Flags,
905
906 const Flags = packed struct {
907 abs: bool = false,
908 weak: bool = false,
909 tlv: bool = false,
910 };
911};
912
913const std = @import("std");
914const assert = std.debug.assert;
915const fs = std.fs;
916const fmt = std.fmt;
917const log = std.log.scoped(.link);
918const macho = std.macho;
919const math = std.math;
920const mem = std.mem;
921const Allocator = mem.Allocator;
922const Path = std.Build.Cache.Path;
923const Writer = std.Io.Writer;
924
925const Dylib = @This();
926const File = @import("file.zig").File;
927const LibStub = tapi.LibStub;
928const LoadCommandIterator = macho.LoadCommandIterator;
929const MachO = @import("../MachO.zig");
930const Symbol = @import("Symbol.zig");
931const Tbd = tapi.Tbd;
932const fat = @import("fat.zig");
933const tapi = @import("../tapi.zig");
934const trace = @import("../../tracy.zig").trace;