Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
16 changes: 12 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,16 @@ 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());
// SAFETY: `to_w_path` NUL-terminates `wbuf`; `hFile` is reserved (NULL);
// `dwFlags = 0` is equivalent to `LoadLibraryW`.
let p = unsafe {
bun_windows_sys::kernel32::LoadLibraryExW(wpath.as_ptr(), core::ptr::null_mut(), 0)
};
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 @@
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 () => {

Check failure on line 672 in test/js/bun/ffi/ffi.test.js

View check run for this annotation

Claude / Claude Code Review

New non-ASCII dlopen test will fail on Windows ARM64 CI

This test will fail on the Windows ARM64 CI lane: `bun:ffi`'s `dlopen` is gated on `ENABLE_TINYCC` (src/runtime/ffi/ffi_body.rs:1428), and TinyCC is disabled for `windows && arm64` (scripts/build/config.ts:869), so the subprocess will throw before printing anything and `exitCode: 0` won't match. Change the guard to `it.skipIf(!isWindows || isArm64)` — the same convention this file already uses via `isFFIUnavailable` at line 719 and in cc.test.ts / ffi-error-messages.test.ts / ffi-viewSource-non-
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