master
1const std = @import("std.zig");
2const builtin = @import("builtin");
3const mem = std.mem;
4const testing = std.testing;
5const elf = std.elf;
6const windows = std.os.windows;
7const native_os = builtin.os.tag;
8const posix = std.posix;
9
10/// Cross-platform dynamic library loading and symbol lookup.
11/// Platform-specific functionality is available through the `inner` field.
12pub const DynLib = struct {
13 const InnerType = switch (native_os) {
14 .linux => if (!builtin.link_libc or builtin.abi == .musl and builtin.link_mode == .static)
15 ElfDynLib
16 else
17 DlDynLib,
18 .windows => WindowsDynLib,
19 .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos, .freebsd, .netbsd, .openbsd, .dragonfly, .illumos => DlDynLib,
20 else => struct {
21 const open = @compileError("unsupported platform");
22 const openZ = @compileError("unsupported platform");
23 },
24 };
25
26 inner: InnerType,
27
28 pub const Error = ElfDynLibError || DlDynLibError || WindowsDynLibError;
29
30 /// Trusts the file. Malicious file will be able to execute arbitrary code.
31 pub fn open(path: []const u8) Error!DynLib {
32 return .{ .inner = try InnerType.open(path) };
33 }
34
35 /// Trusts the file. Malicious file will be able to execute arbitrary code.
36 pub fn openZ(path_c: [*:0]const u8) Error!DynLib {
37 return .{ .inner = try InnerType.openZ(path_c) };
38 }
39
40 /// Trusts the file.
41 pub fn close(self: *DynLib) void {
42 return self.inner.close();
43 }
44
45 pub fn lookup(self: *DynLib, comptime T: type, name: [:0]const u8) ?T {
46 return self.inner.lookup(T, name);
47 }
48};
49
50// The link_map structure is not completely specified beside the fields
51// reported below, any libc is free to store additional data in the remaining
52// space.
53// An iterator is provided in order to traverse the linked list in a idiomatic
54// fashion.
55const LinkMap = extern struct {
56 l_addr: usize,
57 l_name: [*:0]const u8,
58 l_ld: ?*elf.Dyn,
59 l_next: ?*LinkMap,
60 l_prev: ?*LinkMap,
61
62 pub const Iterator = struct {
63 current: ?*LinkMap,
64
65 pub fn end(self: *Iterator) bool {
66 return self.current == null;
67 }
68
69 pub fn next(self: *Iterator) ?*LinkMap {
70 if (self.current) |it| {
71 self.current = it.l_next;
72 return it;
73 }
74 return null;
75 }
76 };
77};
78
79const RDebug = extern struct {
80 r_version: i32,
81 r_map: ?*LinkMap,
82 r_brk: usize,
83 r_ldbase: usize,
84};
85
86/// TODO fix comparisons of extern symbol pointers so we don't need this helper function.
87pub fn get_DYNAMIC() ?[*]const elf.Dyn {
88 return @extern([*]const elf.Dyn, .{
89 .name = "_DYNAMIC",
90 .linkage = .weak,
91 .visibility = .hidden,
92 });
93}
94
95pub fn linkmap_iterator() error{InvalidExe}!LinkMap.Iterator {
96 const _DYNAMIC = get_DYNAMIC() orelse {
97 // No PT_DYNAMIC means this is a statically-linked non-PIE program.
98 return .{ .current = null };
99 };
100
101 const link_map_ptr = init: {
102 var i: usize = 0;
103 while (_DYNAMIC[i].d_tag != elf.DT_NULL) : (i += 1) {
104 switch (_DYNAMIC[i].d_tag) {
105 elf.DT_DEBUG => {
106 const ptr = @as(?*RDebug, @ptrFromInt(_DYNAMIC[i].d_val));
107 if (ptr) |r_debug| {
108 if (r_debug.r_version != 1) return error.InvalidExe;
109 break :init r_debug.r_map;
110 }
111 },
112 elf.DT_PLTGOT => {
113 const ptr = @as(?[*]usize, @ptrFromInt(_DYNAMIC[i].d_val));
114 if (ptr) |got_table| {
115 // The address to the link_map structure is stored in
116 // the second slot
117 break :init @as(?*LinkMap, @ptrFromInt(got_table[1]));
118 }
119 },
120 else => {},
121 }
122 }
123 return .{ .current = null };
124 };
125
126 return .{ .current = link_map_ptr };
127}
128
129/// Separated to avoid referencing `ElfDynLib`, because its field types may not
130/// be valid on other targets.
131const ElfDynLibError = error{
132 FileTooBig,
133 NotElfFile,
134 NotDynamicLibrary,
135 MissingDynamicLinkingInformation,
136 ElfStringSectionNotFound,
137 ElfSymSectionNotFound,
138 ElfHashTableNotFound,
139 Canceled,
140 Streaming,
141} || posix.OpenError || posix.MMapError;
142
143pub const ElfDynLib = struct {
144 strings: [*:0]u8,
145 syms: [*]elf.Sym,
146 hash_table: HashTable,
147 versym: ?[*]elf.Versym,
148 verdef: ?*elf.Verdef,
149 memory: []align(std.heap.page_size_min) u8,
150
151 pub const Error = ElfDynLibError;
152
153 const HashTable = union(enum) {
154 dt_hash: [*]posix.Elf_Symndx,
155 dt_gnu_hash: *elf.gnu_hash.Header,
156 };
157
158 fn openPath(path: []const u8) !std.fs.Dir {
159 if (path.len == 0) return error.NotDir;
160 var parts = std.mem.tokenizeScalar(u8, path, '/');
161 var parent = if (path[0] == '/') try std.fs.cwd().openDir("/", .{}) else std.fs.cwd();
162 while (parts.next()) |part| {
163 const child = try parent.openDir(part, .{});
164 parent.close();
165 parent = child;
166 }
167 return parent;
168 }
169
170 fn resolveFromSearchPath(search_path: []const u8, file_name: []const u8, delim: u8) ?posix.fd_t {
171 var paths = std.mem.tokenizeScalar(u8, search_path, delim);
172 while (paths.next()) |p| {
173 var dir = openPath(p) catch continue;
174 defer dir.close();
175 const fd = posix.openat(dir.fd, file_name, .{
176 .ACCMODE = .RDONLY,
177 .CLOEXEC = true,
178 }, 0) catch continue;
179 return fd;
180 }
181 return null;
182 }
183
184 fn resolveFromParent(dir_path: []const u8, file_name: []const u8) ?posix.fd_t {
185 var dir = std.fs.cwd().openDir(dir_path, .{}) catch return null;
186 defer dir.close();
187 return posix.openat(dir.fd, file_name, .{
188 .ACCMODE = .RDONLY,
189 .CLOEXEC = true,
190 }, 0) catch null;
191 }
192
193 // This implements enough to be able to load system libraries in general
194 // Places where it differs from dlopen:
195 // - DT_RPATH of the calling binary is not used as a search path
196 // - DT_RUNPATH of the calling binary is not used as a search path
197 // - /etc/ld.so.cache is not read
198 fn resolveFromName(path_or_name: []const u8) !posix.fd_t {
199 // If filename contains a slash ("/"), then it is interpreted as a (relative or absolute) pathname
200 if (std.mem.indexOfScalarPos(u8, path_or_name, 0, '/')) |_| {
201 return posix.open(path_or_name, .{ .ACCMODE = .RDONLY, .CLOEXEC = true }, 0);
202 }
203
204 // Only read LD_LIBRARY_PATH if the binary is not setuid/setgid
205 if (std.os.linux.geteuid() == std.os.linux.getuid() and
206 std.os.linux.getegid() == std.os.linux.getgid())
207 {
208 if (posix.getenvZ("LD_LIBRARY_PATH")) |ld_library_path| {
209 if (resolveFromSearchPath(ld_library_path, path_or_name, ':')) |fd| {
210 return fd;
211 }
212 }
213 }
214
215 // Lastly the directories /lib and /usr/lib are searched (in this exact order)
216 if (resolveFromParent("/lib", path_or_name)) |fd| return fd;
217 if (resolveFromParent("/usr/lib", path_or_name)) |fd| return fd;
218 return error.FileNotFound;
219 }
220
221 /// Trusts the file. Malicious file will be able to execute arbitrary code.
222 pub fn open(path: []const u8) Error!ElfDynLib {
223 const fd = try resolveFromName(path);
224 defer posix.close(fd);
225
226 const file: std.fs.File = .{ .handle = fd };
227 const stat = try file.stat();
228 const size = std.math.cast(usize, stat.size) orelse return error.FileTooBig;
229
230 const page_size = std.heap.pageSize();
231
232 // This one is to read the ELF info. We do more mmapping later
233 // corresponding to the actual LOAD sections.
234 const file_bytes = try posix.mmap(
235 null,
236 mem.alignForward(usize, size, page_size),
237 posix.PROT.READ,
238 .{ .TYPE = .PRIVATE },
239 fd,
240 0,
241 );
242 defer posix.munmap(file_bytes);
243
244 const eh = @as(*elf.Ehdr, @ptrCast(file_bytes.ptr));
245 if (!mem.eql(u8, eh.e_ident[0..4], elf.MAGIC)) return error.NotElfFile;
246 if (eh.e_type != elf.ET.DYN) return error.NotDynamicLibrary;
247
248 const elf_addr = @intFromPtr(file_bytes.ptr);
249
250 // Iterate over the program header entries to find out the
251 // dynamic vector as well as the total size of the virtual memory.
252 var maybe_dynv: ?[*]usize = null;
253 var virt_addr_end: usize = 0;
254 {
255 var i: usize = 0;
256 var ph_addr: usize = elf_addr + eh.e_phoff;
257 while (i < eh.e_phnum) : ({
258 i += 1;
259 ph_addr += eh.e_phentsize;
260 }) {
261 const ph = @as(*elf.Phdr, @ptrFromInt(ph_addr));
262 switch (ph.p_type) {
263 elf.PT_LOAD => virt_addr_end = @max(virt_addr_end, ph.p_vaddr + ph.p_memsz),
264 elf.PT_DYNAMIC => maybe_dynv = @as([*]usize, @ptrFromInt(elf_addr + ph.p_offset)),
265 else => {},
266 }
267 }
268 }
269 const dynv = maybe_dynv orelse return error.MissingDynamicLinkingInformation;
270
271 // Reserve the entire range (with no permissions) so that we can do MAP.FIXED below.
272 const all_loaded_mem = try posix.mmap(
273 null,
274 virt_addr_end,
275 posix.PROT.NONE,
276 .{ .TYPE = .PRIVATE, .ANONYMOUS = true },
277 -1,
278 0,
279 );
280 errdefer posix.munmap(all_loaded_mem);
281
282 const base = @intFromPtr(all_loaded_mem.ptr);
283
284 // Now iterate again and actually load all the program sections.
285 {
286 var i: usize = 0;
287 var ph_addr: usize = elf_addr + eh.e_phoff;
288 while (i < eh.e_phnum) : ({
289 i += 1;
290 ph_addr += eh.e_phentsize;
291 }) {
292 const ph = @as(*elf.Phdr, @ptrFromInt(ph_addr));
293 switch (ph.p_type) {
294 elf.PT_LOAD => {
295 // The VirtAddr may not be page-aligned; in such case there will be
296 // extra nonsense mapped before/after the VirtAddr,MemSiz
297 const aligned_addr = (base + ph.p_vaddr) & ~(@as(usize, page_size) - 1);
298 const extra_bytes = (base + ph.p_vaddr) - aligned_addr;
299 const extended_memsz = mem.alignForward(usize, ph.p_memsz + extra_bytes, page_size);
300 const ptr = @as([*]align(std.heap.page_size_min) u8, @ptrFromInt(aligned_addr));
301 const prot = elfToMmapProt(ph.p_flags);
302 if ((ph.p_flags & elf.PF_W) == 0) {
303 // If it does not need write access, it can be mapped from the fd.
304 _ = try posix.mmap(
305 ptr,
306 extended_memsz,
307 prot,
308 .{ .TYPE = .PRIVATE, .FIXED = true },
309 fd,
310 ph.p_offset - extra_bytes,
311 );
312 } else {
313 const sect_mem = try posix.mmap(
314 ptr,
315 extended_memsz,
316 prot,
317 .{ .TYPE = .PRIVATE, .FIXED = true, .ANONYMOUS = true },
318 -1,
319 0,
320 );
321 @memcpy(sect_mem[0..ph.p_filesz], file_bytes[0..ph.p_filesz]);
322 }
323 },
324 else => {},
325 }
326 }
327 }
328
329 var maybe_strings: ?[*:0]u8 = null;
330 var maybe_syms: ?[*]elf.Sym = null;
331 var maybe_hashtab: ?[*]posix.Elf_Symndx = null;
332 var maybe_gnu_hash: ?*elf.gnu_hash.Header = null;
333 var maybe_versym: ?[*]elf.Versym = null;
334 var maybe_verdef: ?*elf.Verdef = null;
335
336 {
337 var i: usize = 0;
338 while (dynv[i] != 0) : (i += 2) {
339 const p = base + dynv[i + 1];
340 switch (dynv[i]) {
341 elf.DT_STRTAB => maybe_strings = @ptrFromInt(p),
342 elf.DT_SYMTAB => maybe_syms = @ptrFromInt(p),
343 elf.DT_HASH => maybe_hashtab = @ptrFromInt(p),
344 elf.DT_GNU_HASH => maybe_gnu_hash = @ptrFromInt(p),
345 elf.DT_VERSYM => maybe_versym = @ptrFromInt(p),
346 elf.DT_VERDEF => maybe_verdef = @ptrFromInt(p),
347 else => {},
348 }
349 }
350 }
351
352 const hash_table: HashTable = if (maybe_gnu_hash) |gnu_hash|
353 .{ .dt_gnu_hash = gnu_hash }
354 else if (maybe_hashtab) |hashtab|
355 .{ .dt_hash = hashtab }
356 else
357 return error.ElfHashTableNotFound;
358
359 return .{
360 .memory = all_loaded_mem,
361 .strings = maybe_strings orelse return error.ElfStringSectionNotFound,
362 .syms = maybe_syms orelse return error.ElfSymSectionNotFound,
363 .hash_table = hash_table,
364 .versym = maybe_versym,
365 .verdef = maybe_verdef,
366 };
367 }
368
369 /// Trusts the file. Malicious file will be able to execute arbitrary code.
370 pub fn openZ(path_c: [*:0]const u8) Error!ElfDynLib {
371 return open(mem.sliceTo(path_c, 0));
372 }
373
374 /// Trusts the file
375 pub fn close(self: *ElfDynLib) void {
376 posix.munmap(self.memory);
377 self.* = undefined;
378 }
379
380 pub fn lookup(self: *const ElfDynLib, comptime T: type, name: [:0]const u8) ?T {
381 if (self.lookupAddress("", name)) |symbol| {
382 return @as(T, @ptrFromInt(symbol));
383 } else {
384 return null;
385 }
386 }
387
388 pub const GnuHashSection32 = struct {
389 symoffset: u32,
390 bloom_shift: u32,
391 bloom: []u32,
392 buckets: []u32,
393 chain: [*]elf.gnu_hash.ChainEntry,
394
395 pub fn fromPtr(header: *elf.gnu_hash.Header) @This() {
396 const header_offset = @intFromPtr(header);
397 const bloom_offset = header_offset + @sizeOf(elf.gnu_hash.Header);
398 const buckets_offset = bloom_offset + header.bloom_size * @sizeOf(u32);
399 const chain_offset = buckets_offset + header.nbuckets * @sizeOf(u32);
400
401 const bloom_ptr: [*]u32 = @ptrFromInt(bloom_offset);
402 const buckets_ptr: [*]u32 = @ptrFromInt(buckets_offset);
403 const chain_ptr: [*]elf.gnu_hash.ChainEntry = @ptrFromInt(chain_offset);
404
405 return .{
406 .symoffset = header.symoffset,
407 .bloom_shift = header.bloom_shift,
408 .bloom = bloom_ptr[0..header.bloom_size],
409 .buckets = buckets_ptr[0..header.nbuckets],
410 .chain = chain_ptr,
411 };
412 }
413 };
414
415 pub const GnuHashSection64 = struct {
416 symoffset: u32,
417 bloom_shift: u32,
418 bloom: []u64,
419 buckets: []u32,
420 chain: [*]elf.gnu_hash.ChainEntry,
421
422 pub fn fromPtr(header: *elf.gnu_hash.Header) @This() {
423 const header_offset = @intFromPtr(header);
424 const bloom_offset = header_offset + @sizeOf(elf.gnu_hash.Header);
425 const buckets_offset = bloom_offset + header.bloom_size * @sizeOf(u64);
426 const chain_offset = buckets_offset + header.nbuckets * @sizeOf(u32);
427
428 const bloom_ptr: [*]u64 = @ptrFromInt(bloom_offset);
429 const buckets_ptr: [*]u32 = @ptrFromInt(buckets_offset);
430 const chain_ptr: [*]elf.gnu_hash.ChainEntry = @ptrFromInt(chain_offset);
431
432 return .{
433 .symoffset = header.symoffset,
434 .bloom_shift = header.bloom_shift,
435 .bloom = bloom_ptr[0..header.bloom_size],
436 .buckets = buckets_ptr[0..header.nbuckets],
437 .chain = chain_ptr,
438 };
439 }
440 };
441
442 /// ElfDynLib specific
443 /// Returns the address of the symbol
444 pub fn lookupAddress(self: *const ElfDynLib, vername: []const u8, name: []const u8) ?usize {
445 const maybe_versym = if (self.verdef == null) null else self.versym;
446
447 const OK_TYPES = (1 << elf.STT_NOTYPE | 1 << elf.STT_OBJECT | 1 << elf.STT_FUNC | 1 << elf.STT_COMMON);
448 const OK_BINDS = (1 << elf.STB_GLOBAL | 1 << elf.STB_WEAK | 1 << elf.STB_GNU_UNIQUE);
449
450 switch (self.hash_table) {
451 .dt_hash => |hashtab| {
452 var i: usize = 0;
453 while (i < hashtab[1]) : (i += 1) {
454 if (0 == (@as(u32, 1) << @as(u5, @intCast(self.syms[i].st_info & 0xf)) & OK_TYPES)) continue;
455 if (0 == (@as(u32, 1) << @as(u5, @intCast(self.syms[i].st_info >> 4)) & OK_BINDS)) continue;
456 if (0 == self.syms[i].st_shndx) continue;
457 if (!mem.eql(u8, name, mem.sliceTo(self.strings + self.syms[i].st_name, 0))) continue;
458 if (maybe_versym) |versym| {
459 if (!checkver(self.verdef.?, versym[i], vername, self.strings))
460 continue;
461 }
462 return @intFromPtr(self.memory.ptr) + self.syms[i].st_value;
463 }
464 },
465 .dt_gnu_hash => |gnu_hash_header| {
466 const GnuHashSection = switch (@bitSizeOf(usize)) {
467 32 => GnuHashSection32,
468 64 => GnuHashSection64,
469 else => |bit_size| @compileError("Unsupported bit size " ++ bit_size),
470 };
471
472 const gnu_hash_section: GnuHashSection = .fromPtr(gnu_hash_header);
473 const hash = elf.gnu_hash.calculate(name);
474
475 const bloom_index = (hash / @bitSizeOf(usize)) % gnu_hash_header.bloom_size;
476 const bloom_val = gnu_hash_section.bloom[bloom_index];
477
478 const bit_index_0 = hash % @bitSizeOf(usize);
479 const bit_index_1 = (hash >> @intCast(gnu_hash_header.bloom_shift)) % @bitSizeOf(usize);
480
481 const one: usize = 1;
482 const bit_mask: usize = (one << @intCast(bit_index_0)) | (one << @intCast(bit_index_1));
483
484 if (bloom_val & bit_mask != bit_mask) {
485 // Symbol is not in bloom filter, so it definitely isn't here.
486 return null;
487 }
488
489 const bucket_index = hash % gnu_hash_header.nbuckets;
490 const chain_index = gnu_hash_section.buckets[bucket_index] - gnu_hash_header.symoffset;
491
492 const chains = gnu_hash_section.chain;
493 const hash_as_entry: elf.gnu_hash.ChainEntry = @bitCast(hash);
494
495 var current_index = chain_index;
496 var at_end_of_chain = false;
497 while (!at_end_of_chain) : (current_index += 1) {
498 const current_entry = chains[current_index];
499 at_end_of_chain = current_entry.end_of_chain;
500
501 if (current_entry.hash != hash_as_entry.hash) continue;
502
503 // check that symbol matches
504 const symbol_index = current_index + gnu_hash_header.symoffset;
505 const symbol = self.syms[symbol_index];
506
507 if (0 == (@as(u32, 1) << @as(u5, @intCast(symbol.st_info & 0xf)) & OK_TYPES)) continue;
508 if (0 == (@as(u32, 1) << @as(u5, @intCast(symbol.st_info >> 4)) & OK_BINDS)) continue;
509 if (0 == symbol.st_shndx) continue;
510
511 const symbol_name = mem.sliceTo(self.strings + symbol.st_name, 0);
512 if (!mem.eql(u8, name, symbol_name)) {
513 continue;
514 }
515
516 if (maybe_versym) |versym| {
517 if (!checkver(self.verdef.?, versym[symbol_index], vername, self.strings)) {
518 continue;
519 }
520 }
521
522 return @intFromPtr(self.memory.ptr) + symbol.st_value;
523 }
524 },
525 }
526
527 return null;
528 }
529
530 fn elfToMmapProt(elf_prot: u64) u32 {
531 var result: u32 = posix.PROT.NONE;
532 if ((elf_prot & elf.PF_R) != 0) result |= posix.PROT.READ;
533 if ((elf_prot & elf.PF_W) != 0) result |= posix.PROT.WRITE;
534 if ((elf_prot & elf.PF_X) != 0) result |= posix.PROT.EXEC;
535 return result;
536 }
537};
538
539fn checkver(def_arg: *elf.Verdef, vsym_arg: elf.Versym, vername: []const u8, strings: [*:0]u8) bool {
540 var def = def_arg;
541 const vsym_index = vsym_arg.VERSION;
542 while (true) {
543 if (0 == (def.flags & elf.VER_FLG_BASE) and @intFromEnum(def.ndx) == vsym_index) break;
544 if (def.next == 0) return false;
545 def = @ptrFromInt(@intFromPtr(def) + def.next);
546 }
547 const aux: *elf.Verdaux = @ptrFromInt(@intFromPtr(def) + def.aux);
548 return mem.eql(u8, vername, mem.sliceTo(strings + aux.name, 0));
549}
550
551test "ElfDynLib" {
552 if (native_os != .linux) {
553 return error.SkipZigTest;
554 }
555
556 try testing.expectError(error.FileNotFound, ElfDynLib.open("invalid_so.so"));
557}
558
559/// Separated to avoid referencing `WindowsDynLib`, because its field types may not
560/// be valid on other targets.
561const WindowsDynLibError = error{
562 FileNotFound,
563 InvalidPath,
564} || windows.LoadLibraryError;
565
566pub const WindowsDynLib = struct {
567 pub const Error = WindowsDynLibError;
568
569 dll: windows.HMODULE,
570
571 pub fn open(path: []const u8) Error!WindowsDynLib {
572 return openEx(path, .none);
573 }
574
575 /// WindowsDynLib specific
576 /// Opens dynamic library with specified library loading flags.
577 pub fn openEx(path: []const u8, flags: windows.LoadLibraryFlags) Error!WindowsDynLib {
578 const path_w = windows.sliceToPrefixedFileW(null, path) catch return error.InvalidPath;
579 return openExW(path_w.span().ptr, flags);
580 }
581
582 pub fn openZ(path_c: [*:0]const u8) Error!WindowsDynLib {
583 return openExZ(path_c, .none);
584 }
585
586 /// WindowsDynLib specific
587 /// Opens dynamic library with specified library loading flags.
588 pub fn openExZ(path_c: [*:0]const u8, flags: windows.LoadLibraryFlags) Error!WindowsDynLib {
589 const path_w = windows.cStrToPrefixedFileW(null, path_c) catch return error.InvalidPath;
590 return openExW(path_w.span().ptr, flags);
591 }
592
593 /// WindowsDynLib specific
594 pub fn openW(path_w: [*:0]const u16) Error!WindowsDynLib {
595 return openExW(path_w, .none);
596 }
597
598 /// WindowsDynLib specific
599 /// Opens dynamic library with specified library loading flags.
600 pub fn openExW(path_w: [*:0]const u16, flags: windows.LoadLibraryFlags) Error!WindowsDynLib {
601 var offset: usize = 0;
602 if (path_w[0] == '\\' and path_w[1] == '?' and path_w[2] == '?' and path_w[3] == '\\') {
603 // + 4 to skip over the \??\
604 offset = 4;
605 }
606
607 return .{
608 .dll = try windows.LoadLibraryExW(path_w + offset, flags),
609 };
610 }
611
612 pub fn close(self: *WindowsDynLib) void {
613 windows.FreeLibrary(self.dll);
614 self.* = undefined;
615 }
616
617 pub fn lookup(self: *WindowsDynLib, comptime T: type, name: [:0]const u8) ?T {
618 if (windows.kernel32.GetProcAddress(self.dll, name.ptr)) |addr| {
619 return @as(T, @ptrCast(@alignCast(addr)));
620 } else {
621 return null;
622 }
623 }
624};
625
626/// Separated to avoid referencing `DlDynLib`, because its field types may not
627/// be valid on other targets.
628const DlDynLibError = error{ FileNotFound, NameTooLong };
629
630pub const DlDynLib = struct {
631 pub const Error = DlDynLibError;
632
633 handle: *anyopaque,
634
635 pub fn open(path: []const u8) Error!DlDynLib {
636 const path_c = try posix.toPosixPath(path);
637 return openZ(&path_c);
638 }
639
640 pub fn openZ(path_c: [*:0]const u8) Error!DlDynLib {
641 return .{
642 .handle = std.c.dlopen(path_c, .{ .LAZY = true }) orelse {
643 return error.FileNotFound;
644 },
645 };
646 }
647
648 pub fn close(self: *DlDynLib) void {
649 switch (posix.errno(std.c.dlclose(self.handle))) {
650 .SUCCESS => return,
651 else => unreachable,
652 }
653 self.* = undefined;
654 }
655
656 pub fn lookup(self: *DlDynLib, comptime T: type, name: [:0]const u8) ?T {
657 // dlsym (and other dl-functions) secretly take shadow parameter - return address on stack
658 // https://gcc.gnu.org/bugzilla/show_bug.cgi?id=66826
659 if (@call(.never_tail, std.c.dlsym, .{ self.handle, name.ptr })) |symbol| {
660 return @as(T, @ptrCast(@alignCast(symbol)));
661 } else {
662 return null;
663 }
664 }
665
666 /// DlDynLib specific
667 /// Returns human readable string describing most recent error than occurred from `lookup`
668 /// or `null` if no error has occurred since initialization or when `getError` was last called.
669 pub fn getError() ?[:0]const u8 {
670 return mem.span(std.c.dlerror());
671 }
672};
673
674test "dynamic_library" {
675 const libname = switch (native_os) {
676 .linux, .freebsd, .openbsd, .illumos => "invalid_so.so",
677 .windows => "invalid_dll.dll",
678 .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => "invalid_dylib.dylib",
679 else => return error.SkipZigTest,
680 };
681
682 try testing.expectError(error.FileNotFound, DynLib.open(libname));
683 try testing.expectError(error.FileNotFound, DynLib.openZ(libname.ptr));
684}