diff --git a/src/bun.js/api/ffi.zig b/src/bun.js/api/ffi.zig index 5533908a4d..470cd8daf1 100644 --- a/src/bun.js/api/ffi.zig +++ b/src/bun.js/api/ffi.zig @@ -2,6 +2,30 @@ const debug = Output.scoped(.TCC, .visible); extern fn pthread_jit_write_protect_np(enable: c_int) void; +/// Get the last dynamic library loading error message in a cross-platform way. +/// On POSIX systems, this calls dlerror(). +/// On Windows, this uses GetLastError() and formats the error message. +/// Returns an allocated string that must be freed by the caller. +fn getDlError(allocator: std.mem.Allocator) ![]const u8 { + if (Environment.isWindows) { + // On Windows, we need to use GetLastError() and FormatMessageW() + const err = bun.windows.GetLastError(); + const err_int = @intFromEnum(err); + + // For now, just return the error code as we'd need to implement FormatMessageW in Zig + // This is still better than a generic message + return try std.fmt.allocPrint(allocator, "error code {d}", .{err_int}); + } else { + // On POSIX systems, use dlerror() to get the actual system error + const msg = if (std.c.dlerror()) |err_ptr| + std.mem.span(err_ptr) + else + "unknown error"; + // Return a copy since dlerror() string is not stable + return try allocator.dupe(u8, msg); + } +} + /// Run a function that needs to write to JIT-protected memory. /// /// This is dangerous as it allows overwriting executable regions of memory. @@ -1020,10 +1044,20 @@ pub const FFI = struct { const backup_name = Fs.FileSystem.instance.abs(&[1]string{name}); // if that fails, try resolving the filepath relative to the current working directory break :brk std.DynLib.open(backup_name) catch { - // Then, if that fails, report an error. + // Then, if that fails, report an error with the library name and system error + const dlerror_buf = getDlError(bun.default_allocator) catch null; + defer if (dlerror_buf) |buf| bun.default_allocator.free(buf); + const dlerror_msg = dlerror_buf orelse "unknown error"; + + const msg = bun.handleOom(std.fmt.allocPrint( + bun.default_allocator, + "Failed to open library \"{s}\": {s}", + .{ name, dlerror_msg }, + )); + defer bun.default_allocator.free(msg); const system_error = jsc.SystemError{ .code = bun.String.cloneUTF8(@tagName(.ERR_DLOPEN_FAILED)), - .message = bun.String.cloneUTF8("Failed to open library. This is usually caused by a missing library or an invalid library path."), + .message = bun.String.cloneUTF8(msg), .syscall = bun.String.cloneUTF8("dlopen"), }; return system_error.toErrorInstance(global); @@ -1154,7 +1188,7 @@ pub const FFI = struct { const function_name = function.base_name.?; if (function.symbol_from_dynamic_library == null) { - const ret = global.toInvalidArguments("Symbol for \"{s}\" not found", .{bun.asByteSlice(function_name)}); + const ret = global.toInvalidArguments("Symbol \"{s}\" is missing a \"ptr\" field. When using linkSymbols() or CFunction(), you must provide a \"ptr\" field with the memory address of the native function.", .{bun.asByteSlice(function_name)}); for (symbols.values()) |*value| { allocator.free(@constCast(bun.asByteSlice(value.base_name.?))); value.arg_types.clearAndFree(allocator); diff --git a/src/js/bun/ffi.ts b/src/js/bun/ffi.ts index eda1d924e8..07886f902b 100644 --- a/src/js/bun/ffi.ts +++ b/src/js/bun/ffi.ts @@ -526,6 +526,7 @@ function cc(options) { function linkSymbols(options) { const result = nativeLinkSymbols(options); + if (Error.isError(result)) throw result; for (let key in result.symbols) { var symbol = result.symbols[key]; diff --git a/test/js/bun/ffi/ffi-error-messages.test.ts b/test/js/bun/ffi/ffi-error-messages.test.ts new file mode 100644 index 0000000000..07ccd0970f --- /dev/null +++ b/test/js/bun/ffi/ffi-error-messages.test.ts @@ -0,0 +1,65 @@ +import { dlopen, linkSymbols } from "bun:ffi"; +import { describe, expect, test } from "bun:test"; +import { isMusl } from "harness"; + +describe("FFI error messages", () => { + test("dlopen shows library name when library cannot be opened", () => { + // Try to open a non-existent library + try { + dlopen("libnonexistent12345.so", { + test: { + args: [], + returns: "int", + }, + }); + expect.unreachable("Should have thrown an error"); + } catch (err: any) { + // Error message should include the library name + expect(err.message).toContain("libnonexistent12345.so"); + expect(err.message).toMatch(/Failed to open library/i); + } + }); + + test("dlopen shows which symbol is missing when symbol not found", () => { + // Use appropriate system library for the platform + const libName = + process.platform === "win32" + ? "kernel32.dll" // Windows system library + : process.platform === "darwin" + ? "libSystem.B.dylib" // macOS system library + : isMusl + ? process.arch === "arm64" + ? "libc.musl-aarch64.so.1" // ARM64 musl + : "libc.musl-x86_64.so.1" // x86_64 musl + : "libc.so.6"; // glibc + + // Try to load a non-existent symbol + try { + dlopen(libName, { + this_symbol_definitely_does_not_exist_in_the_system_library: { + args: [], + returns: "int", + }, + }); + expect.unreachable("Should have thrown an error"); + } catch (err: any) { + // Error message should include the symbol name + expect(err.message).toMatch(/this_symbol_definitely_does_not_exist_in_the_system_library/); + // Error message should include some reference to the library or symbol not found + expect(err.message).toMatch(/Symbol.*not found|symbol.*not found/i); + } + }); + + test("linkSymbols shows helpful error when ptr is missing", () => { + // Try to use linkSymbols without providing a valid ptr + expect(() => { + linkSymbols({ + myFunction: { + args: [], + returns: "int", + // Missing 'ptr' field - this should give a helpful error + }, + }); + }).toThrow(/myFunction.*ptr.*(linkSymbols|CFunction)/); + }); +});