DynLib no longer supports Windows, and what is the intended solution?

After uploaded 0.16.0, based on the release notes, I have found that DynLib no longer supports Windows, yet the only issues I could find is this one from the codeberg. Although someone has proposed a workaround, I had a couple of questions come in mind:

  • Is the change to DynLib temporary? Will it be removed or it will stay at this state of excluding windows?
  • given the release note: DynLib: removed Windows support. Now users must use LoadLibraryExW and GetProcAddress directly, which is probably what they were already doing anyway. Since I don’t seem to find much info related to the alternative solutions besides the one from the codeberg, may I know anyone actually doing this and managed to get it work? I wish to see a couple more examples before I update my library that needs to use DynLib.
1 Like

Hey, I had the exact same issue and I ended up with the following code:

const std = @import("std");
const Allocator = std.mem.Allocator;
const DynLib = std.DynLib;
const builtin = @import("builtin");
const oom = @import("misc").oom;

const HMODULE = *anyopaque;
const FARPROC = *anyopaque;

extern "kernel32" fn LoadLibraryExW(
    lpLibFileName: [*:0]const u16,
    hFile: ?*anyopaque,
    dwFlags: u32,
) callconv(.winapi) ?HMODULE;

extern "kernel32" fn FreeLibrary(hLibModule: HMODULE) callconv(.winapi) i32;
extern "kernel32" fn GetProcAddress(hModule: HMODULE, lpProcName: [*:0]const u8) callconv(.winapi) ?FARPROC;
extern "kernel32" fn GetLastError() callconv(.winapi) u32;

lib: Lib,

const os = builtin.os.tag;
const LOAD_LIBRARY_SEARCH_DEFAULT_DIRS = 0x1000;

const Self = @This();
const Lib = if (os == .windows) HMODULE else DynLib;
const Error = error{ UnsupportedOS, LoadFailed };

pub fn open(alloc: Allocator, path: []const u8, name: []const u8) Error!Self {
    const full_path = std.fmt.allocPrint(alloc, "{s}{s}{s}", .{
        path,
        std.Io.Dir.path.sep_str,
        try libName(alloc, name),
    }) catch oom();

    const lib_handle = switch (os) {
        .windows => win: {
            var path_utf16: [std.fs.max_path_bytes:0]u16 = undefined;
            const n = std.unicode.utf8ToUtf16Le(&path_utf16, full_path) catch return error.LoadFailed;
            path_utf16[n] = 0;

            break :win LoadLibraryExW(
                @ptrCast(path_utf16[0..n].ptr),
                null,
                LOAD_LIBRARY_SEARCH_DEFAULT_DIRS,
            ) orelse return error.LoadFailed;
        },
        else => DynLib.open(full_path) catch return error.LoadFailed,
    };

    return .{
        .lib = lib_handle,
    };
}

fn libName(alloc: Allocator, name: []const u8) Error![]const u8 {
    return switch (os) {
        .linux => std.fmt.allocPrint(alloc, "lib{s}.so", .{name}) catch oom(),
        .macos => std.fmt.allocPrint(alloc, "lib{s}.dylib", .{name}) catch oom(),
        .windows => std.fmt.allocPrint(alloc, "{s}.dll", .{name}) catch oom(),
        else => error.UnsupportedOS,
    };
}

pub fn lookup(self: *Self, T: type, name: [:0]const u8) ?T {
    return switch (os) {
        .windows => @ptrCast(GetProcAddress(self.lib, name.ptr) orelse return null),
        else => self.lib.lookup(T, name),
    };
}

pub fn close(self: *Self) void {
    switch (os) {
        .windows => _ = FreeLibrary(self.lib),
        else => self.lib.close(),
    }
}

It works on windows, linux and macos for my use cases. Hope this helps!

11 Likes

Thank you very much, and this version looks cleaner! I will further evaluate both versions to see if I could get my dynamic libraries back to the working condition.

Update:
Marked yours as the solution! With a bit of modification to the libName function, now my library is working once again!

1 Like

After I have updated my library and I have found that the code have memory leak on these part if you are not using arena allocator:

const full_path = std.fmt.allocPrint(alloc, "{s}{s}{s}", .{
    path,
    std.Io.Dir.path.sep_str,
    try libName(alloc, name),
}) catch oom();

For people who don’t use arena for this process, since these slices are not exposed from the caller, you may pull the libName out as another variable so that you can free them individually:

const lib_name = try libName(alloc, name);
const full_path = std.fmt.allocPrint(alloc, "{s}{s}{s}", .{
    path,
    std.Io.Dir.path.sep_str,
    lib_name,
}) catch oom();
defer alloc.free(full_path);
defer alloc.free(lib_name);

With this modification, it should be leak free for all allocators; nonetheless, thanks for your solution since you have saved my two important projects for a community.

2 Likes