diff --git a/src/sys/lib.rs b/src/sys/lib.rs index cc90af006966..39158993e7f6 100644 --- a/src/sys/lib.rs +++ b/src/sys/lib.rs @@ -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 { let mut buf = bun_paths::PathBuffer::default(); // `std.DynLib.open` returns `error.NameTooLong`; never truncate (could @@ -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)] { @@ -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()) } } } diff --git a/test/js/bun/ffi/ffi.test.js b/test/js/bun/ffi/ffi.test.js index 1a66bae5d943..2546e1d1fcfd 100644 --- a/test/js/bun/ffi/ffi.test.js +++ b/test/js/bun/ffi/ffi.test.js @@ -666,6 +666,47 @@ it("dlopen throws an error instead of returning it", () => { expect(err).toBeTruthy(); }); +// TinyCC, which implements JSCallback and CFunction, is unavailable on Windows ARM64. +const isFFIUnavailable = isWindows && isArm64; + +// 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 || isFFIUnavailable)("dlopen accepts non-ASCII library paths on Windows", async () => { + 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(/^\./); }); @@ -677,9 +718,6 @@ it(".ptr is not leaked", () => { } }); -// TinyCC, which implements JSCallback and CFunction, is unavailable on Windows ARM64. -const isFFIUnavailable = isWindows && isArm64; - // Runs in a subprocess: `bun test`'s exit path does not finalize the CFunction's native handle, // which the ASan lane's leak checker then reports against this file. it.skipIf(isFFIUnavailable)("JSCallback exceptions propagate out of the native call", async () => {