Files
bun.sh/test/js/bun/ffi/ffi-error-messages.test.ts
robobun dc36d5601c Improve FFI error messages when symbol is missing ptr field (#23585)
### What does this PR do?

Fixes unhelpful FFI error messages that made debugging extremely
difficult. The user reported that when dlopen fails, the error doesn't
tell you which library failed or why.

**Before:**
```
Failed to open library. This is usually caused by a missing library or an invalid library path.
```

**After:**
```
Failed to open library "libnonexistent.so": /path/libnonexistent.so: cannot open shared object file: No such file or directory
```

### How did you verify your code works?

1. **Cross-platform compilation verified**
- Ran `bun run zig:check-all` - all platforms compile successfully
(Windows, macOS x86_64/arm64, Linux x86_64/arm64 glibc/musl)

2. **Added comprehensive regression tests**
(`test/regression/issue/dlopen-missing-symbol-error.test.ts`)
   -  Tests dlopen error shows library name when it can't be opened
   -  Tests dlopen error shows symbol name when symbol isn't found
   -  Tests linkSymbols shows helpful error when ptr is missing
   -  Tests handle both glibc and musl libc systems

3. **Manually tested error messages**
   - Missing library: Shows full path and "No such file or directory"
   - Invalid library: Shows "invalid ELF header"
   - Missing symbol: Shows symbol and library name
   - linkSymbols without ptr: Shows helpful explanation

### Implementation Details

1. **Created cross-platform getDlError() helper**
(src/bun.js/api/ffi.zig:8-21)
- On POSIX: Calls `std.c.dlerror()` to get actual system error message
- On Windows: Returns generic message (detailed errors handled in C++
layer via `GetLastError()` + `FormatMessageW()`)
- Follows the pattern established in `BunProcess.cpp` for dlopen error
handling

2. **Improved error messages**
   - dlopen errors now include library name and system error details
   - linkSymbols errors explain the ptr field requirement clearly
   - Symbol lookup errors already showed both symbol and library name

3. **Fixed linkSymbols error propagation** (src/js/bun/ffi.ts:529)
   - Added missing `if (Error.isError(result)) throw result;` check
   - Now consistent with dlopen which already had this check

### Example Error Messages

- **Missing library:** `Failed to open library "libnonexistent.so":
cannot open shared object file: No such file or directory`
- **Invalid library:** `Failed to open library "/etc/passwd": invalid
ELF header`
- **Missing symbol:** `Symbol "nonexistent_func" not found in
"libc.so.6"`
- **Missing ptr:** `Symbol "myFunc" is missing a "ptr" field. When using
linkSymbols() or CFunction()...`

Fixes the issue mentioned in:
https://fxtwitter.com/hassanalinali/status/1977710104334963015

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
2025-10-14 14:38:17 -07:00

66 lines
2.2 KiB
TypeScript

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)/);
});
});