diff --git a/docs/runtime/c-compiler.mdx b/docs/runtime/c-compiler.mdx index fdb185d9fb8f..bdf09f037776 100644 --- a/docs/runtime/c-compiler.mdx +++ b/docs/runtime/c-compiler.mdx @@ -53,27 +53,28 @@ What is the answer to the universe? 42 `cc` supports the same `FFIType` values as [`dlopen`](/runtime/ffi). -| `FFIType` | C Type | Aliases | -| ---------- | -------------- | --------------------------- | -| cstring | `char*` | | -| function | `(void*)(*)()` | `fn`, `callback` | -| ptr | `void*` | `pointer`, `void*`, `char*` | -| i8 | `int8_t` | `int8_t` | -| i16 | `int16_t` | `int16_t` | -| i32 | `int32_t` | `int32_t`, `int` | -| i64 | `int64_t` | `int64_t` | -| i64_fast | `int64_t` | | -| u8 | `uint8_t` | `uint8_t` | -| u16 | `uint16_t` | `uint16_t` | -| u32 | `uint32_t` | `uint32_t` | -| u64 | `uint64_t` | `uint64_t` | -| u64_fast | `uint64_t` | | -| f32 | `float` | `float` | -| f64 | `double` | `double` | -| bool | `bool` | | -| char | `char` | | -| napi_env | `napi_env` | | -| napi_value | `napi_value` | | +| `FFIType` | C Type | Aliases | +| ---------- | -------------- | ----------------------------- | +| cstring | `char*` | | +| function | `(void*)(*)()` | `fn`, `callback` | +| ptr | `void*` | `pointer`, `void*`, `char*` | +| i8 | `int8_t` | `int8_t` | +| i16 | `int16_t` | `int16_t` | +| i32 | `int32_t` | `int32_t`, `int` | +| i64 | `int64_t` | `int64_t` | +| i64_fast | `int64_t` | | +| u8 | `uint8_t` | `uint8_t` | +| u16 | `uint16_t` | `uint16_t` | +| u32 | `uint32_t` | `uint32_t` | +| u64 | `uint64_t` | `uint64_t`, `usize`, `size_t` | +| u64_fast | `uint64_t` | | +| f32 | `float` | `float` | +| f64 | `double` | `double` | +| bool | `bool` | | +| char | `char` | | +| void | `void` | | +| napi_env | `napi_env` | | +| napi_value | `napi_value` | | ### Strings, objects, and non-primitive types @@ -200,3 +201,22 @@ cc({ }, }); ``` + +#### `include: string | string[]` + +Use `include` to add directories to the compiler's header search path. Equivalent to the `-I` option in gcc/clang. + +```ts +type Include = string | string[]; + +cc({ + source: "hello.c", + include: ["./vendor/include"], + symbols: { + hello: { + args: [], + returns: "int", + }, + }, +}); +``` diff --git a/docs/runtime/ffi.mdx b/docs/runtime/ffi.mdx index 7687e58431af..ca3186c51f18 100644 --- a/docs/runtime/ffi.mdx +++ b/docs/runtime/ffi.mdx @@ -125,35 +125,81 @@ clang++ -dynamiclib add.cpp -o libadd.dylib --- +## Compiling C from JavaScript (`cc`) + +Instead of compiling a shared library ahead of time, you can compile and run ISO C11 source code directly from JavaScript with `cc`. Bun compiles the source with [TinyCC](https://github.com/TinyCC/tinycc) and exposes the requested `symbols` as JavaScript functions, just like `dlopen`. + +```ts +import { cc } from "bun:ffi"; +import source from "./hello.c" with { type: "file" }; + +const { + symbols: { hello }, +} = cc({ + source, + symbols: { + hello: { + returns: "cstring", + args: [], + }, + }, +}); + +console.log(`${hello()}`); // "Hello, World!" +``` + +```c hello.c icon="file-code" +const char* hello() { + return "Hello, World!"; +} +``` + +See the [C Compiler](/runtime/c-compiler) page for the full `cc` reference, including the `source`, `symbols`, `define`, `include`, `library`, and `flags` options. + +--- + ## FFI types The following `FFIType` values are supported. -| `FFIType` | C Type | Aliases | -| ---------- | -------------- | --------------------------- | -| buffer | `char*` | | -| cstring | `char*` | | -| function | `(void*)(*)()` | `fn`, `callback` | -| ptr | `void*` | `pointer`, `void*`, `char*` | -| i8 | `int8_t` | `int8_t` | -| i16 | `int16_t` | `int16_t` | -| i32 | `int32_t` | `int32_t`, `int` | -| i64 | `int64_t` | `int64_t` | -| i64_fast | `int64_t` | | -| u8 | `uint8_t` | `uint8_t` | -| u16 | `uint16_t` | `uint16_t` | -| u32 | `uint32_t` | `uint32_t` | -| u64 | `uint64_t` | `uint64_t` | -| u64_fast | `uint64_t` | | -| f32 | `float` | `float` | -| f64 | `double` | `double` | -| bool | `bool` | | -| char | `char` | | -| napi_env | `napi_env` | | -| napi_value | `napi_value` | | +| `FFIType` | C Type | Aliases | +| ---------- | -------------- | ----------------------------- | +| buffer | `char*` | | +| cstring | `char*` | | +| function | `(void*)(*)()` | `fn`, `callback` | +| ptr | `void*` | `pointer`, `void*`, `char*` | +| i8 | `int8_t` | `int8_t` | +| i16 | `int16_t` | `int16_t` | +| i32 | `int32_t` | `int32_t`, `int` | +| i64 | `int64_t` | `int64_t` | +| i64_fast | `int64_t` | | +| u8 | `uint8_t` | `uint8_t` | +| u16 | `uint16_t` | `uint16_t` | +| u32 | `uint32_t` | `uint32_t` | +| u64 | `uint64_t` | `uint64_t`, `usize`, `size_t` | +| u64_fast | `uint64_t` | | +| f32 | `float` | `float` | +| f64 | `double` | `double` | +| bool | `bool` | | +| char | `char` | | +| void | `void` | | +| napi_env | `napi_env` | | +| napi_value | `napi_value` | | `buffer` arguments must be a `TypedArray` or `DataView`. +Note: `void` is only valid as a `returns` type — it is the default return type and cannot be used as an argument. + +Note: `usize` and `isize` are pointer-sized integer aliases for `u64` and `i64`. Both are accepted at runtime, but only `"usize"` is currently in the TypeScript string types — if you need `isize` while type-checking, use `i64` (or `FFIType.i64`). + +--- + +## Node-API types + +The `napi_env` and `napi_value` types let `bun:ffi` interoperate with [Node-API](/runtime/node-api). A C function that receives a `napi_env` argument is called with Bun's current N-API environment, and a function that returns `napi_value` has its result converted directly into a JavaScript value — which is useful for passing strings, objects, and other non-primitive values that don't map 1:1 to C types. + +These types work with `cc`. See [Strings, objects, and non-primitive types](/runtime/c-compiler#strings-objects-and-non-primitive-types) on the C Compiler page for worked examples. + --- ## Strings @@ -233,7 +279,7 @@ import { CFunction } from "bun:ffi"; let myNativeLibraryGetVersion = /* somehow, you got this pointer */ -const getVersion = new CFunction({ +const getVersion = CFunction({ returns: "cstring", args: [], ptr: myNativeLibraryGetVersion, @@ -335,7 +381,7 @@ const onResolve = new JSCallback(arg => arg === 42, { returns: "bool", args: ["i32"], }); -const setOnResolve = new CFunction({ +const setOnResolve = CFunction({ returns: "bool", args: ["function"], ptr: myNativeLibrarySetOnResolve, @@ -352,6 +398,38 @@ setOnResolve(onResolve); --- +## Viewing generated bindings + +Bun just-in-time compiles C wrappers for each symbol. To inspect the generated C code — for debugging a binding or out of curiosity — use `viewSource`. Given a map of symbols it returns an array of strings (one per symbol); for a single callback definition, pass `true` as the second argument to get a single string. + +```ts +import { viewSource } from "bun:ffi"; + +// the generated wrappers for a set of symbols +const [getVersionSource] = viewSource( + { + getVersion: { + returns: "cstring", + args: [], + }, + }, + false, +); + +// the generated wrapper for a single callback +const callbackSource = viewSource( + { + returns: "bool", + args: ["ptr", "usize"], + }, + true, +); +``` + +You typically won't need this unless there's a bug in the FFI bindings generator. + +--- + ## Pointers Bun represents [pointers]() as a `number` in JavaScript. @@ -416,19 +494,22 @@ console.log( The `read` function behaves similarly to `DataView`, but it's usually faster because it doesn't need to create a `DataView` or `ArrayBuffer`. -| `FFIType` | `read` function | -| --------- | --------------- | -| ptr | `read.ptr` | -| i8 | `read.i8` | -| i16 | `read.i16` | -| i32 | `read.i32` | -| i64 | `read.i64` | -| u8 | `read.u8` | -| u16 | `read.u16` | -| u32 | `read.u32` | -| u64 | `read.u64` | -| f32 | `read.f32` | -| f64 | `read.f64` | +| Type | `read` function | +| ------ | --------------- | +| ptr | `read.ptr` | +| intptr | `read.intptr` | +| i8 | `read.i8` | +| i16 | `read.i16` | +| i32 | `read.i32` | +| i64 | `read.i64` | +| u8 | `read.u8` | +| u16 | `read.u16` | +| u32 | `read.u32` | +| u64 | `read.u64` | +| f32 | `read.f32` | +| f64 | `read.f64` | + +`read.ptr` and `read.intptr` both read a pointer-sized integer. They are `read` helpers only — `intptr` is not an `FFIType`, so it can't be used in `args`/`returns`. ### Memory management diff --git a/packages/bun-types/ffi.d.ts b/packages/bun-types/ffi.d.ts index 8c365189a310..457f6990a9ea 100644 --- a/packages/bun-types/ffi.d.ts +++ b/packages/bun-types/ffi.d.ts @@ -420,6 +420,7 @@ declare module "bun:ffi" { ["cstring"]: FFIType.cstring; ["function"]: FFIType.pointer; // for now ["usize"]: FFIType.uint64_t; // for now + ["size_t"]: FFIType.uint64_t; // for now ["callback"]: FFIType.pointer; // for now ["napi_env"]: FFIType.napi_env; ["napi_value"]: FFIType.napi_value; @@ -602,13 +603,11 @@ declare module "bun:ffi" { * }, * }, * }); - * // "Hello, World!" - * console.log(hello()); + * console.log(`${hello()}`); // "Hello, World!" * ``` * * `./hello.c`: * ```c - * #include * const char* hello() { * return "Hello, World!"; * } @@ -702,7 +701,7 @@ declare module "bun:ffi" { * ```js * import {CFunction} from 'bun:ffi'; * - * const getVersion = new CFunction({ + * const getVersion = CFunction({ * returns: "cstring", * args: [], * ptr: myNativeLibraryGetVersion, diff --git a/src/js/bun/ffi.ts b/src/js/bun/ffi.ts index 390e6f58f8d4..95464b89c1a7 100644 --- a/src/js/bun/ffi.ts +++ b/src/js/bun/ffi.ts @@ -17,6 +17,9 @@ const FFIType = { "15": 15, "16": 16, "17": 17, + "18": 18, + "19": 19, + "20": 20, bool: 11, c_int: 5, c_uint: 6, @@ -45,6 +48,7 @@ const FFIType = { uint64_t: 8, uint8_t: 2, usize: 8, + size_t: 8, "void*": 12, ptr: 12, pointer: 12, @@ -337,6 +341,11 @@ ffiWrappers[FFIType.function] = `{ return ptr; }`; +// Node-API arguments are passed through as raw JSValues. `napi_value` is read +// directly on the native side, and `napi_env` is substituted there for the +// module's env, so neither must go through the default `val|0` coercion. +ffiWrappers[FFIType.napi_env] = ffiWrappers[FFIType.napi_value] = "val"; + function FFIBuilder(params, returnType, functionToCall, name) { const hasReturnType = typeof FFIType[returnType] === "number" && FFIType[returnType as string] !== FFIType.void; var paramNames = new Array(params.length); @@ -497,19 +506,27 @@ function cc(options) { const result = ccFn(options); if (Error.isError(result)) throw result; + // `source` may be an array of files; use the first one for stack-trace labels. + const displayPath = $isJSArray(path) ? path[0] : path; + for (let key in result.symbols) { var symbol = result.symbols[key]; - if (options[key]?.args?.length || FFIType[options[key]?.returns as string] === FFIType.cstring) { + // `cc` nests the symbol definitions under `options.symbols` (unlike + // `dlopen`/`linkSymbols`, where `options` is the symbol map itself), so we + // must read from `options.symbols[key]` — otherwise the `cstring` return + // wrapper is never applied and the function returns a raw pointer. + const definition = options.symbols?.[key]; + if (definition?.args?.length || FFIType[definition?.returns as string] === FFIType.cstring) { result.symbols[key] = FFIBuilder( - options[key].args ?? [], - options[key].returns ?? FFIType.void, + definition.args ?? [], + definition.returns ?? FFIType.void, symbol, // in stacktraces: // instead of // "/usr/lib/sqlite3.so" // we want // "sqlite3_get_version() - sqlit3.so" - path.includes("/") ? `${key} (${path.split("/").pop()})` : `${key} (${path})`, + displayPath.includes("/") ? `${key} (${displayPath.split("/").pop()})` : `${key} (${displayPath})`, ); } else { // consistentcy diff --git a/test/integration/bun-types/fixture/ffi.ts b/test/integration/bun-types/fixture/ffi.ts index 9f18e37c70f2..995d2e506763 100644 --- a/test/integration/bun-types/fixture/ffi.ts +++ b/test/integration/bun-types/fixture/ffi.ts @@ -25,6 +25,11 @@ const lib = dlopen( args: [FFIType.function], returns: FFIType.function, }, + size_type: { + // "size_t" is accepted as a string alias of uint64_t + args: ["size_t"], + returns: "size_t", + }, allArgs: { args: [ FFIType.char, // string @@ -71,6 +76,9 @@ tsd.expectType(lib.symbols.ptr_type(ptr)); tsd.expectType(lib.symbols.fn_type(new JSCallback(() => {}, {}))); +// "size_t" resolves to uint64_t: accepts number|bigint, returns bigint +tsd.expectType(lib.symbols.size_type(1n)); + function _arg( ...params: [ number, diff --git a/test/js/bun/ffi/cc.test.ts b/test/js/bun/ffi/cc.test.ts index 99a1b5eac1a1..5aa565679433 100644 --- a/test/js/bun/ffi/cc.test.ts +++ b/test/js/bun/ffi/cc.test.ts @@ -1,4 +1,4 @@ -import { cc, CString, JSCallback, ptr, type FFIFunction, type Library } from "bun:ffi"; +import { cc, CString, FFIType, JSCallback, ptr, type FFIFunction, type Library } from "bun:ffi"; import { afterAll, beforeAll, describe, expect, it } from "bun:test"; import { promises as fs } from "fs"; import { bunEnv, bunExe, isArm64, isASAN, isWindows, normalizeBunSnapshot, tempDir, tempDirWithFiles } from "harness"; @@ -96,6 +96,147 @@ describe.skipIf(isASAN || isFFIUnavailable)("given an add(a, b) function", () => }); }); // +// Regression test: the compiler accepts `"size_t"` as a type, but it was +// missing from the JS `FFIType` map, so once `FFIBuilder` became reachable for +// `cc` a `"size_t"` arg/return threw `Unsupported type size_t`. +describe.skipIf(isASAN || isFFIUnavailable)("given an inc(size_t) function", () => { + let dir: string; + let res: Library<{ inc: { args: ["size_t"]; returns: "size_t" } }>; + + beforeAll(() => { + dir = tempDirWithFiles("bun-ffi-cc-size_t", { + "inc.c": /* c */ ` + #include + size_t inc(size_t n) { + return n + 1; + } + `, + }); + res = cc({ + source: path.join(dir, "inc.c"), + symbols: { + inc: { + args: ["size_t"], + returns: "size_t", + }, + }, + }); + }); + + afterAll(async () => { + res.close(); + await fs.rm(dir, { recursive: true, force: true }); + }); + + it("accepts `size_t` as an argument and return type", () => { + // `size_t` maps to `uint64_t`, so it round-trips as a bigint. + expect(res.symbols.inc(41n)).toBe(42n); + }); +}); // + +// Regression test: `cc` read symbol definitions from `options[key]` instead of +// `options.symbols[key]`, so the `cstring` return wrapper was never applied and +// the function returned a raw pointer number instead of a `CString`. +describe.skipIf(isASAN || isFFIUnavailable)("given a hello() function returning a cstring", () => { + let dir: string; + let res: Library<{ hello: { args: []; returns: "cstring" } }>; + + beforeAll(() => { + dir = tempDirWithFiles("bun-ffi-cc-cstring", { + "hello.c": /* c */ ` + const char* hello() { + return "Hello, World!"; + } + `, + }); + res = cc({ + source: path.join(dir, "hello.c"), + symbols: { + hello: { + returns: "cstring", + args: [], + }, + }, + }); + }); + + afterAll(async () => { + res.close(); + await fs.rm(dir, { recursive: true, force: true }); + }); + + it("returns a CString, not a raw pointer number", () => { + const result = res.symbols.hello(); + expect(typeof result).not.toBe("number"); + expect(result).toBeInstanceOf(CString); + expect(String(result)).toBe("Hello, World!"); + }); +}); // + +// Regression test: once the `cstring` fix made `FFIBuilder` reachable for `cc`, +// `napi_value` arguments were coerced with the default `val|0` wrapper (turning +// them into `0` / throwing on BigInt) instead of being passed through as-is. +// `napi_value` is an opaque pointer, so an identity function needs no Node-API +// headers and links nothing — it just round-trips the raw JSValue. +describe.skipIf(isASAN || isFFIUnavailable)("given an identity(napi_value) function", () => { + let dir: string; + let res: Library<{ identity: { args: ["napi_value"]; returns: "napi_value" } }>; + + beforeAll(() => { + dir = tempDirWithFiles("bun-ffi-cc-napi-arg", { + "identity.c": /* c */ ` + typedef struct napi_value__* napi_value; + napi_value identity(napi_value value) { + return value; + } + `, + }); + res = cc({ + source: path.join(dir, "identity.c"), + symbols: { + identity: { + args: ["napi_value"], + returns: "napi_value", + }, + }, + }); + }); + + afterAll(async () => { + res.close(); + await fs.rm(dir, { recursive: true, force: true }); + }); + + it("returns the same JS value it was passed", () => { + // Without the fix, the argument is coerced (e.g. to the number 0), so the + // round-tripped value would not be identical to the input. + for (const value of [{ a: 1 }, "hello", 12345, [1, 2, 3], true]) { + expect(Object.is(res.symbols.identity(value), value)).toBe(true); + } + }); + + it("works when the type is given as the numeric FFIType enum", () => { + // `FFIType.napi_value` is 19; the reverse lookup `FFIType[19]` must resolve + // so `FFIBuilder` picks the right wrapper instead of throwing/coercing. + // Reuses the identical source compiled in `beforeAll` (cleaned up by `afterAll`). + const numericRes = cc({ + source: path.join(dir, "identity.c"), + symbols: { + identity: { + args: [FFIType.napi_value], + returns: FFIType.napi_value, + }, + }, + }); + try { + const value = { b: 2 }; + expect(Object.is(numericRes.symbols.identity(value), value)).toBe(true); + } finally { + numericRes.close(); + } + }); +}); // + describe("given a source file with syntax errors", () => { const source = /* c */ ` int add(int a, int b) {