Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
8 changes: 7 additions & 1 deletion src/js/bun/ffi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ var ffi = globalThis.Bun.FFI;
const ptr = (arg1, arg2) => (typeof arg2 === "undefined" ? ffi.ptr(arg1) : ffi.ptr(arg1, arg2));
const toBuffer = ffi.toBuffer;
const toArrayBuffer = ffi.toArrayBuffer;
const viewSource = ffi.viewSource;
const nativeViewSource = ffi.viewSource;

const BunCString = ffi.CString;
const nativeLinkSymbols = ffi.linkSymbols;
Expand Down Expand Up @@ -521,6 +521,12 @@ function cc(options) {
return result;
}

function viewSource(symbols, isCallback?) {
const result = nativeViewSource(symbols, isCallback);
if (Error.isError(result)) throw result;
return result;
}

function linkSymbols(options) {
const result = nativeLinkSymbols(options);
if (Error.isError(result)) throw result;
Expand Down
63 changes: 55 additions & 8 deletions test/js/bun/ffi/ffi-viewSource-non-object.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,63 @@
import { viewSource } from "bun:ffi";
import { describe, expect, test } from "bun:test";
import { isArm64, isWindows } from "harness";

const isFFIUnavailable = isWindows && isArm64;

// Captures what a call throws, or undefined if it returned normally. Written
// explicitly so the assertions below distinguish a thrown Error from a
// returned one regardless of toThrow()'s handling of returned Errors.
function thrown(fn: () => unknown): unknown {
try {
fn();
} catch (e) {
return e;
}
return undefined;
}

describe.skipIf(isFFIUnavailable)("FFI viewSource", () => {
test("rejects non-object symbol descriptor values", () => {
// These should throw a TypeError because each symbol descriptor
// must be an object like { args: [...], returns: "void" }.
// Previously, non-object values like numbers or strings would
// cause a debug assertion failure (crash) in generateSymbolForFunction.
expect(() => Bun.FFI.viewSource({ myFunc: 42 })).toThrow("Expected an object");
expect(() => Bun.FFI.viewSource({ myFunc: "not_an_object" })).toThrow("Expected an object");
expect(() => Bun.FFI.viewSource({ myFunc: true })).toThrow("Expected an object");
test("throws on non-object symbol descriptor values", () => {
// Each symbol descriptor must be an object like { args: [...], returns: "void" }.
// Previously, non-object values like numbers or strings would cause a debug
// assertion failure (crash) in generateSymbolForFunction; after that crash
// was fixed, viewSource returned the TypeError instead of throwing it.
for (const value of [42, "not_an_object", true] as const) {
const err = thrown(() => viewSource({ myFunc: value as any }));
expect(err).toBeInstanceOf(TypeError);
expect((err as TypeError).message).toContain("Expected an object");
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
});

test("throws on an unknown FFI type", () => {
const err = thrown(() => viewSource({ foo: { args: ["bogus_type" as any], returns: "void" } }));
expect(err).toBeInstanceOf(TypeError);
expect((err as TypeError).message).toContain("bogus_type");
});

test("throws on a non-object options argument", () => {
for (const value of [null, undefined, 42] as const) {
const err = thrown(() => viewSource(value as any));
expect(err).toBeInstanceOf(TypeError);
}
});

test("throws on a non-object callback descriptor", () => {
for (const value of [null, undefined, 42, "str"] as const) {
const err = thrown(() => viewSource(value as any, true));
expect(err).toBeInstanceOf(TypeError);
expect((err as TypeError).message).toContain("Expected an object");
}
});

test("returns the generated source for a valid descriptor", () => {
const src = viewSource({ foo: { args: ["i32"], returns: "i32" } });
expect(src).toBeArray();
expect(src).toHaveLength(1);
expect(src[0]).toContain("JSFunctionCall");

const cbSrc = viewSource({ args: ["i32"], returns: "i32" }, true);
expect(typeof cbSrc).toBe("string");
expect(cbSrc).toContain("my_callback_function");
});
});
Loading