Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 25 additions & 4 deletions src/sys/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6095,7 +6095,7 @@ unsafe impl Send for DynLib {}
// synchronized, so `&DynLib` may be shared across threads.
unsafe impl Sync for DynLib {}
impl DynLib {
/// `dlopen(path, RTLD_LAZY)` / `LoadLibraryA(path)`.
/// `dlopen(path, RTLD_LAZY)` / `LoadLibraryW(path)`.
pub fn open(path: &[u8]) -> core::result::Result<Self, bun_core::Error> {
let mut buf = bun_paths::PathBuffer::default();
// `std.DynLib.open` returns `error.NameTooLong`; never truncate (could
Expand Down Expand Up @@ -6161,7 +6161,7 @@ pub mod RTLD {
pub const LOCAL: i32 = 0;
}

/// `dlopen(filename, flags)`. Windows → `LoadLibraryA`.
/// `dlopen(filename, flags)`. Windows → `LoadLibraryExW` (UTF-8 → UTF-16).
pub fn dlopen(filename: &ZStr, flags: i32) -> Option<*mut c_void> {
#[cfg(unix)]
{
Expand All @@ -6172,8 +6172,29 @@ pub fn dlopen(filename: &ZStr, flags: i32) -> Option<*mut c_void> {
#[cfg(windows)]
{
let _ = flags;
// SAFETY: filename is NUL-terminated.
let p = unsafe { bun_windows_sys::externs::LoadLibraryA(filename.as_ptr()) };
// `filename` is UTF-8; the `A` entry point would decode it as the
// system ANSI codepage and mangle any non-ASCII byte. Widen and use
// the `W` entry point like every other Windows path in this crate.
let mut wbuf = bun_paths::w_path_buffer_pool::get();
let wpath = bun_paths::string_paths::to_w_path(&mut wbuf, filename.as_bytes());
// Match libuv `uv_dlopen` (and Bun's own `process.dlopen`): request
// altered search so dependent DLLs resolve next to the loaded module.
// MSDN documents that flag as undefined for relative paths, so only
// set it when absolute; bare names keep the standard search order.
const LOAD_WITH_ALTERED_SEARCH_PATH: u32 = 0x0000_0008;
let dw_flags = if bun_paths::is_absolute_windows(filename.as_bytes()) {
LOAD_WITH_ALTERED_SEARCH_PATH
} else {
0
};
// SAFETY: `to_w_path` NUL-terminates `wbuf`; `hFile` is reserved (NULL).
let p = unsafe {
bun_windows_sys::kernel32::LoadLibraryExW(
wpath.as_ptr(),
core::ptr::null_mut(),
dw_flags,
)
};
if p.is_null() { None } else { Some(p.cast()) }
}
}
Expand Down
38 changes: 38 additions & 0 deletions test/js/bun/ffi/ffi.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,44 @@ it("dlopen throws an error instead of returning it", () => {
expect(err).toBeTruthy();
});

// Windows: dlopen must accept paths with non-ASCII characters. Previously the
// path was handed to LoadLibraryA as UTF-8, which the OS decodes as the system
// ANSI codepage, so any non-ASCII byte mangled the path.
it.skipIf(!isWindows)("dlopen accepts non-ASCII library paths on Windows", async () => {
Comment thread
robobun marked this conversation as resolved.
Outdated
const fixture = `
const { dlopen, FFIType } = require("bun:ffi");
const { mkdirSync, copyFileSync } = require("node:fs");
const { join } = require("node:path");

const src = join(process.env.SystemRoot || "C:\\\\Windows", "System32", "version.dll");
const results = {};
for (const name of ["caf\\u00e9", "\\u65e5\\u672c\\u8a9e"]) {
const dir = join(process.env.FIXTURE_DIR, "bun-ffi-" + name);
mkdirSync(dir, { recursive: true });
const dll = join(dir, "version.dll");
copyFileSync(src, dll);
const lib = dlopen(dll, {
GetFileVersionInfoSizeW: { args: [FFIType.ptr, FFIType.ptr], returns: FFIType.u32 },
});
results[name] = typeof lib.symbols.GetFileVersionInfoSizeW;
lib.close();
}
console.log(JSON.stringify(results));
`;
using dir = tempDir("ffi-dlopen-unicode", {});
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", fixture],
env: { ...bunEnv, FIXTURE_DIR: String(dir) },
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
const results = stdout.startsWith("{") ? JSON.parse(stdout) : stdout;
expect({ results, stderr, exitCode }).toMatchObject({
results: { "caf\u00e9": "function", "\u65e5\u672c\u8a9e": "function" },
exitCode: 0,
});
});

it('suffix does not start with a "."', () => {
expect(suffix).not.toMatch(/^\./);
});
Expand Down
Loading