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}