Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
44 changes: 41 additions & 3 deletions test/js/bun/ffi/ffi.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(/^\./);
});
Expand All @@ -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 () => {
Expand Down
Loading