diff options
-rw-r--r-- | integration/bunjs-only-snippets/buffer.test.js | 4 | ||||
-rw-r--r-- | integration/bunjs-only-snippets/ffi.test.js | 35 | ||||
-rw-r--r-- | packages/bun-types/package.json | 2 | ||||
-rw-r--r-- | packages/bun-types/types.d.ts | 342 | ||||
-rw-r--r-- | src/javascript/jsc/api/FFI.h | 1 | ||||
-rw-r--r-- | src/javascript/jsc/api/bun.zig | 371 | ||||
-rw-r--r-- | src/javascript/jsc/api/ffi.zig | 136 | ||||
-rw-r--r-- | src/javascript/jsc/base.zig | 51 | ||||
-rw-r--r-- | src/javascript/jsc/ffi.exports.js | 7 | ||||
-rw-r--r-- | src/javascript/jsc/javascript.zig | 15 | ||||
-rw-r--r-- | src/linker.zig | 7 | ||||
-rw-r--r-- | types/bun/bun.d.ts | 2 | ||||
-rw-r--r-- | types/bun/ffi.d.ts | 523 | ||||
-rw-r--r-- | types/bun/index.d.ts | 1 | ||||
-rw-r--r-- | types/bun/paths.txt | 1 |
15 files changed, 1277 insertions, 221 deletions
diff --git a/integration/bunjs-only-snippets/buffer.test.js b/integration/bunjs-only-snippets/buffer.test.js index 0d5821fdc..d1a5c7ff3 100644 --- a/integration/bunjs-only-snippets/buffer.test.js +++ b/integration/bunjs-only-snippets/buffer.test.js @@ -31,10 +31,6 @@ it("Buffer", () => { expect(new Buffer(input).toString("utf8")).toBe(inputs[i]); expect(Array.from(new Buffer(input)).join(",")).toBe(good[i].join(",")); } - for (let i = 0; i < inputs.length; i++) { - var input = inputs[i]; - expect(new Buffer(input, "ucs2").toString("utf8")).toBe(inputs[i]); - } }); it("Buffer.toBuffer throws", () => { diff --git a/integration/bunjs-only-snippets/ffi.test.js b/integration/bunjs-only-snippets/ffi.test.js index 16967fe51..2337a44c8 100644 --- a/integration/bunjs-only-snippets/ffi.test.js +++ b/integration/bunjs-only-snippets/ffi.test.js @@ -1,10 +1,20 @@ import { describe, it, expect } from "bun:test"; import { unsafe } from "bun"; +// +import { + viewSource, + dlopen, + CString, + ptr, + toBuffer, + toArrayBuffer, + FFIType, +} from "bun:ffi"; it("ffi print", () => { - Bun.dlprint({ + viewSource({ add: { - params: ["int32_t", "int32_t"], + args: [FFIType.int], return_type: "int32_t", }, })[0]; @@ -211,7 +221,7 @@ it("ffi run", () => { ptr_should_point_to_42_as_int32_t, }, close, - } = Bun.dlopen("/tmp/bun-ffi-test.dylib", types); + } = dlopen("/tmp/bun-ffi-test.dylib", types); expect(returns_true()).toBe(true); expect(returns_false()).toBe(false); @@ -249,16 +259,17 @@ it("ffi run", () => { expect(add_uint16_t(1, 1)).toBe(2); expect(add_uint32_t(1, 1)).toBe(2); - const ptr = ptr_should_point_to_42_as_int32_t(); - expect(ptr != 0).toBe(true); - expect(typeof ptr === "number").toBe(true); - expect(does_pointer_equal_42_as_int32_t(ptr)).toBe(true); - const buffer = unsafe.bufferFromPtr(ptr, 4); + const cptr = ptr_should_point_to_42_as_int32_t(); + expect(cptr != 0).toBe(true); + expect(typeof cptr === "number").toBe(true); + expect(does_pointer_equal_42_as_int32_t(cptr)).toBe(true); + const buffer = toBuffer(cptr, 0, 4); expect(buffer.readInt32(0)).toBe(42); - expect( - new DataView(unsafe.arrayBufferFromPtr(ptr, 4), 0, 4).getInt32(0, true) - ).toBe(42); - expect(unsafe.arrayBufferToPtr(buffer)).toBe(ptr); + expect(new DataView(toArrayBuffer(cptr, 0, 4), 0, 4).getInt32(0, true)).toBe( + 42 + ); + expect(ptr(buffer)).toBe(cptr); + expect(new CString(cptr, 0, 1)).toBe("*"); close(); }); ``; diff --git a/packages/bun-types/package.json b/packages/bun-types/package.json index 8d6c8e90d..5b6b10ace 100644 --- a/packages/bun-types/package.json +++ b/packages/bun-types/package.json @@ -1,6 +1,6 @@ { "name": "bun-types", - "version": "0.0.78", + "version": "0.0.79", "description": "Type definitions for bun.js", "types": "types.d.ts", "files": [ diff --git a/packages/bun-types/types.d.ts b/packages/bun-types/types.d.ts index ebe8e2e5f..b3dfbc605 100644 --- a/packages/bun-types/types.d.ts +++ b/packages/bun-types/types.d.ts @@ -572,6 +572,22 @@ declare module "bun" { serverNames: Record<string, SSLOptions & SSLAdvancedOptions>; }; + /** + * HTTP & HTTPS Server + * + * To start the server, see {@link serve} + * + * Often, you don't need to interact with this object directly. It exists to help you with the following tasks: + * - Stop the server + * - How many requests are currently being handled? + * + * For performance, Bun pre-allocates most of the data for 2048 concurrent requests. + * That means starting a new server allocates about 500 KB of memory. Try to + * avoid starting and stopping the server often (unless it's a new instance of bun). + * + * Powered by a fork of [uWebSockets](https://github.com/uNetworking/uWebSockets). Thank you @alexhultman. + * + */ interface Server { /** * Stop listening to prevent new connections from being accepted. @@ -877,7 +893,7 @@ declare module "bun" { * * This hashing function balances speed with cryptographic strength. This does not encrypt or decrypt data. * - * Powered by [BoringSSL](https://boringssl.googlesource.com/boringssl) (used in Chromium & Go) + * The implementation uses [BoringSSL](https://boringssl.googlesource.com/boringssl) (used in Chromium & Go) * * The equivalent `openssl` command is: * @@ -897,7 +913,7 @@ declare module "bun" { * * This hashing function balances speed with cryptographic strength. This does not encrypt or decrypt data. * - * Powered by [BoringSSL](https://boringssl.googlesource.com/boringssl) (used in Chromium & Go) + * The implementation uses [BoringSSL](https://boringssl.googlesource.com/boringssl) (used in Chromium & Go) * * The equivalent `openssl` command is: * @@ -909,14 +925,9 @@ declare module "bun" { export function sha(input: StringOrBuffer, encoding: DigestEncoding): string; /** + * This is not the default because it's not cryptographically secure and it's slower than {@link SHA512} * - * Hashing functions for SHA-1 - * - * Powered by [BoringSSL](https://boringssl.googlesource.com/boringssl) (used in Chromium & Go) - * - * SHA-1 is no longer cryptographically secure and it's slower than {@link SHA512}. - * - * Consider using the {@link SHA512_256} instead. + * Consider using the ugly-named {@link SHA512_256} instead */ export class SHA1 extends CryptoHashInterface<SHA1> { constructor(); @@ -926,18 +937,6 @@ declare module "bun" { */ static readonly byteLength: 20; } - - /** - * - * Hashing functions for MD5 - * - * Powered by [BoringSSL](https://boringssl.googlesource.com/boringssl) (used in Chromium & Go) - * - * If you're looking for a fast hashing function, consider {@link hash} - * instead. This is not cryptographically secure and it's slower than - * {@link hash}. The best reason to use it is compatibility. - * - */ export class MD5 extends CryptoHashInterface<MD5> { constructor(); @@ -946,18 +945,6 @@ declare module "bun" { */ static readonly byteLength: 16; } - - /** - * - * Ancient hashing function. Bun maybe shouldn't have included this. - * - * Powered by [BoringSSL](https://boringssl.googlesource.com/boringssl) (used in Chromium & Go) - * - * If you're looking for a fast hashing function, consider {@link hash} - * instead. This is not cryptographically secure and it's slower than - * {@link hash}. The best reason to use it is compatibility. - * - */ export class MD4 extends CryptoHashInterface<MD4> { constructor(); @@ -966,11 +953,6 @@ declare module "bun" { */ static readonly byteLength: 16; } - /** - * Smaller variant of SHA-2 - * - * Powered by [BoringSSL](https://boringssl.googlesource.com/boringssl) (used in Chromium & Go) - */ export class SHA224 extends CryptoHashInterface<SHA224> { constructor(); @@ -979,11 +961,6 @@ declare module "bun" { */ static readonly byteLength: 28; } - /** - * Faster variant of SHA-2, but with bigger output (64 bytes) - * - * Powered by [BoringSSL](https://boringssl.googlesource.com/boringssl) (used in Chromium & Go) - */ export class SHA512 extends CryptoHashInterface<SHA512> { constructor(); @@ -1000,13 +977,6 @@ declare module "bun" { */ static readonly byteLength: 48; } - /** - * SHA-2 (Secure Hash Algorithm 2) is a set of cryptographic hash functions - * designed by the United States National Security Agency (NSA) and first - * published in 2001 - * - * {@link https://en.wikipedia.org/wiki/SHA-2} - */ export class SHA256 extends CryptoHashInterface<SHA256> { constructor(); @@ -1016,9 +986,6 @@ declare module "bun" { static readonly byteLength: 32; } /** - * - * Fast variant of SHA-512, but with smaller output (32 bytes) - * * See also {@link sha} */ export class SHA512_256 extends CryptoHashInterface<SHA512_256> { @@ -1063,6 +1030,268 @@ interface BufferEncodingOption { declare var Bun: typeof import("bun"); + +// ./ffi.d.ts + +/** + * `bun:ffi` lets you efficiently call C functions & FFI functions from JavaScript + * without writing any C code yourself. + * + * ```js + * import {dlopen, CString, ptr} from 'bun:ffi'; + * + * const lib = dlopen('libsqlite3', {}); + * + * + * ``` + * + * This is powered by just-in-time compiling C wrappers + * that convert JavaScript types to C types and back. Internally, + * bun uses [tinycc](https://github.com/TinyCC/tinycc), so a big thanks + * goes to Fabrice Bellard and TinyCC maintainers for making this possible. + * + */ +declare module "bun:ffi" { + export enum FFIType { + char = 0, + + int8_t = 1, + i8 = 1, + + uint8_t = 2, + u8 = 2, + + int16_t = 3, + i16 = 3, + + uint16_t = 4, + u16 = 4, + + /** + * 32-bit signed integer + * + */ + int32_t = 5, + + /** + * 32-bit signed integer + * + * Alias of {@link FFIType.int32_t} + */ + i32 = 5, + /** + * 32-bit signed integer + * + * The same as `int` in C + */ + int = 5, + + uint32_t = 6, + u32 = 6, + + int64_t = 7, + i64 = 7, + + uint64_t = 8, + u64 = 8, + + double = 9, + f64 = 9, + + float = 10, + f32 = 10, + + bool = 11, + + ptr = 12, + pointer = 12, + + void = 13, + } + + type Symbols = Record< + string, + { + /** + * Arguments to a C function + * + * Defaults to an empty array, which means no arguments. + * + * To pass a pointer, use "ptr" or "pointer" as the type name. To get a pointer, see {@link ptr}. + * + * @example + * From JavaScript: + * ```js + * const lib = dlopen('add', { + * // FFIType can be used or you can pass string labels. + * args: [FFIType.i32, "i32"], + * return_type: "i32", + * }); + * lib.symbols.add(1, 2) + * ``` + * In C: + * ```c + * int add(int a, int b) { + * return a + b; + * } + * ``` + */ + args?: FFIType[]; + return_type?: FFIType; + } + >; + + export interface Library { + symbols: Record<string, CallableFunction>; + + /** + * `dlclose` the library, unloading the symbols and freeing memory allocated. + * + * Once called, the library is no longer usable. + * + * Calling a function from a library that has been closed is undefined behavior. + */ + close(): void; + } + + export function dlopen(libraryName: string, symbols: Symbols): Library<T>; + + /** + * Read a pointer as a {@link Buffer} + * + * If `byteLength` is not provided, the pointer is assumed to be 0-terminated. + * + * @param ptr The memory address to read + * @param byteOffset bytes to skip before reading + * @param byteLength bytes to read + * + * While there are some checks to catch invalid pointers, this is a difficult + * thing to do safely. Passing an invalid pointer can crash the program and + * reading beyond the bounds of the pointer will crash the program or cause + * undefined behavior. Use with care! + * + */ + export function toBuffer( + ptr: number, + byteOffset?: number, + byteLength?: number + ): Buffer; + + /** + * Read a pointer as an {@link ArrayBuffer} + * + * If `byteLength` is not provided, the pointer is assumed to be 0-terminated. + * + * @param ptr The memory address to read + * @param byteOffset bytes to skip before reading + * @param byteLength bytes to read + * + * While there are some checks to catch invalid pointers, this is a difficult + * thing to do safely. Passing an invalid pointer can crash the program and + * reading beyond the bounds of the pointer will crash the program or cause + * undefined behavior. Use with care! + */ + export function toArrayBuffer( + ptr: number, + byteOffset?: number, + byteLength?: number + ): ArrayBuffer; + + /** + * Get the pointer backing a {@link TypedArray} or {@link ArrayBuffer} + * + * Use this to pass {@link TypedArray} or {@link ArrayBuffer} to C functions. + * + * This is for use with FFI functions. For performance reasons, FFI will + * not automatically convert typed arrays to C pointers. + * + * @param {TypedArray|ArrayBuffer|DataView} view the typed array or array buffer to get the pointer for + * @param {number} byteOffset optional offset into the view in bytes + * + * @example + * + * From JavaScript: + * ```js + * const array = new Uint8Array(10); + * const rawPtr = ptr(array); + * myCFunction(rawPtr); + * ``` + * To C: + * ```c + * void myCFunction(char* rawPtr) { + * // Do something with rawPtr + * } + * ``` + * + */ + export function ptr( + view: TypedArray | ArrayBufferLike | DataView, + byteOffset?: number + ): number; + + /** + * Get a string from a UTF-8 encoded C string + * If `byteLength` is not provided, the string is assumed to be null-terminated. + * + * @example + * ```js + * var ptr = lib.symbols.getVersion(); + * console.log(new CString(ptr)); + * ``` + * + * @example + * ```js + * var ptr = lib.symbols.getVersion(); + * // print the first 4 characters + * console.log(new CString(ptr, 0, 4)); + * ``` + * + * While there are some checks to catch invalid pointers, this is a difficult + * thing to do safely. Passing an invalid pointer can crash the program and + * reading beyond the bounds of the pointer will crash the program or cause + * undefined behavior. Use with care! + */ + export interface CString { + /** + * Get a string from a UTF-8 encoded C string + * If `byteLength` is not provided, the string is assumed to be null-terminated. + * + * @param ptr The pointer to the C string + * @param byteOffset bytes to skip before reading + * @param byteLength bytes to read + * + * + * @example + * ```js + * var ptr = lib.symbols.getVersion(); + * console.log(new CString(ptr)); + * ``` + * + * @example + * ```js + * var ptr = lib.symbols.getVersion(); + * // print the first 4 characters + * console.log(new CString(ptr, 0, 4)); + * ``` + * + * While there are some checks to catch invalid pointers, this is a difficult + * thing to do safely. Passing an invalid pointer can crash the program and + * reading beyond the bounds of the pointer will crash the program or cause + * undefined behavior. Use with care! + */ + new (ptr: number, byteOffset?: number, byteLength?: number): string; + } + + /** + * View the generated C code for FFI bindings + * + * You probably won't need this unless there's a bug in the FFI bindings + * generator or you're just curious. + */ + export function viewSource(symbols: Symbols): string[]; +} + + // ./fs.d.ts /** @@ -4664,6 +4893,7 @@ declare module "node:fs" { export = fs; } + // ./html-rewriter.d.ts declare namespace HTMLRewriterTypes { @@ -4780,9 +5010,10 @@ declare class HTMLRewriter { transform(input: Response): Response; } + // ./globals.d.ts -type Encoding = "utf8" | "utf-8" | "windows-1252" | "utf-16"; +type Encoding = "utf-8" | "windows-1252" | "utf-16"; interface console { assert(condition?: boolean, ...data: any[]): void; @@ -6208,6 +6439,7 @@ declare var Loader: { resolveSync: (specifier: string, from: string) => string; }; + // ./path.d.ts /** @@ -6415,6 +6647,7 @@ declare module "node:path/win32" { export * from "path/win32"; } + // ./bun-test.d.ts /** @@ -6453,3 +6686,4 @@ declare module "test" { import BunTestModule = require("bun:test"); export = BunTestModule; } + diff --git a/src/javascript/jsc/api/FFI.h b/src/javascript/jsc/api/FFI.h index d5ccf3152..10552b604 100644 --- a/src/javascript/jsc/api/FFI.h +++ b/src/javascript/jsc/api/FFI.h @@ -18,6 +18,7 @@ /* 7.18.1.1 Exact-width integer types */ typedef signed char int8_t; typedef unsigned char uint8_t; +typedef char int8_t; typedef short int16_t; typedef unsigned short uint16_t; typedef int int32_t; diff --git a/src/javascript/jsc/api/bun.zig b/src/javascript/jsc/api/bun.zig index 34e9b7645..1481bb689 100644 --- a/src/javascript/jsc/api/bun.zig +++ b/src/javascript/jsc/api/bun.zig @@ -77,6 +77,7 @@ const VirtualMachine = @import("../javascript.zig").VirtualMachine; const IOTask = JSC.IOTask; const is_bindgen = JSC.is_bindgen; +const max_addressible_memory = std.math.maxInt(u56); threadlocal var css_imports_list_strings: [512]ZigString = undefined; threadlocal var css_imports_list: [512]Api.StringPointer = undefined; @@ -1121,12 +1122,6 @@ pub const Class = NewClass( .sha = .{ .rfn = JSC.wrapWithHasContainer(Crypto.SHA512_256, "hash", false, false), }, - .dlprint = .{ - .rfn = JSC.wrapWithHasContainer(JSC.FFI, "print", false, false), - }, - .dlopen = .{ - .rfn = JSC.wrapWithHasContainer(JSC.FFI, "open", false, false), - }, }, .{ .main = .{ @@ -1208,6 +1203,9 @@ pub const Class = NewClass( .SHA512_256 = .{ .get = Crypto.SHA512_256.getter, }, + .FFI = .{ + .get = FFI.getter, + }, }, ); @@ -1862,112 +1860,10 @@ pub const Unsafe = struct { .arrayBufferToString = .{ .rfn = arrayBufferToString, }, - .arrayBufferToPtr = .{ - .rfn = JSC.wrapWithHasContainer(Unsafe, "arrayBufferToPtr", false, false), - }, - .arrayBufferFromPtr = .{ - .rfn = JSC.wrapWithHasContainer(Unsafe, "arrayBufferFromPtr", false, false), - }, - .bufferFromPtr = .{ - .rfn = JSC.wrapWithHasContainer(Unsafe, "bufferFromPtr", false, false), - }, }, .{}, ); - const ValueOrError = union(enum) { - err: JSValue, - slice: []u8, - }; - - pub fn arrayBufferToPtr(globalThis: *JSGlobalObject, value: JSValue) JSValue { - if (value.isEmpty()) { - return JSC.JSValue.jsNull(); - } - - const array_buffer = value.asArrayBuffer(globalThis) orelse { - return JSC.toInvalidArguments("Expected ArrayBufferView", .{}, globalThis.ref()); - }; - - if (array_buffer.len == 0) { - return JSC.toInvalidArguments("ArrayBufferView must have a length > 0. A pointer to empty memory doesn't work", .{}, globalThis.ref()); - } - - return JSC.JSValue.jsNumber(@bitCast(f64, @ptrToInt(array_buffer.ptr))); - } - - fn getPtrSlice(globalThis: *JSGlobalObject, value: JSValue, valueLength: JSValue) ValueOrError { - if (!value.isNumber()) { - return .{ .err = JSC.toInvalidArguments("ptr must be a number.", .{}, globalThis.ref()) }; - } - - const num = value.asNumber(); - if (num == 0) { - return .{ .err = JSC.toInvalidArguments("ptr cannot be zero, that would segfault Bun :(", .{}, globalThis.ref()) }; - } - - if (!std.math.isFinite(num)) { - return .{ .err = JSC.toInvalidArguments("ptr must be a finite number.", .{}, globalThis.ref()) }; - } - - const addr = @bitCast(usize, num); - - if (addr == 0xDEADBEEF or addr == 0xaaaaaaaa or addr == 0xAAAAAAAA) { - return .{ .err = JSC.toInvalidArguments("ptr to invalid memory, that would segfault Bun :(", .{}, globalThis.ref()) }; - } - - if (!valueLength.isNumber()) { - return .{ .err = JSC.toInvalidArguments("length must be a number.", .{}, globalThis.ref()) }; - } - - if (valueLength.asNumber() == 0.0) { - return .{ .err = JSC.toInvalidArguments("length must be > 0. This usually means a bug in your code.", .{}, globalThis.ref()) }; - } - - const length_i = valueLength.toInt64(); - if (length_i < 0) { - return .{ .err = JSC.toInvalidArguments("length must be > 0. This usually means a bug in your code.", .{}, globalThis.ref()) }; - } - - if (length_i > std.math.maxInt(u48)) { - return .{ .err = JSC.toInvalidArguments("length exceeds max addressable memory. This usually means a bug in your code.", .{}, globalThis.ref()) }; - } - - const length = @intCast(usize, length_i); - - return .{ .slice = @intToPtr([*]u8, addr)[0..length] }; - } - - pub fn arrayBufferFromPtr( - globalThis: *JSGlobalObject, - value: JSValue, - valueLength: JSValue, - ) JSC.JSValue { - switch (getPtrSlice(globalThis, value, valueLength)) { - .err => |erro| { - return erro; - }, - .slice => |slice| { - return JSC.ArrayBuffer.fromBytes(slice, JSC.JSValue.JSType.ArrayBuffer).toJSWithContext(globalThis.ref(), null, null, null); - }, - } - } - - pub fn bufferFromPtr( - globalThis: *JSGlobalObject, - value: JSValue, - valueLength: JSValue, - ) JSC.JSValue { - switch (getPtrSlice(globalThis, value, valueLength)) { - .err => |erro| { - return erro; - }, - .slice => |slice| { - return JSC.JSValue.createBuffer(globalThis, slice, null); - }, - } - } - // For testing the segfault handler pub fn __debug__doSegfault( _: void, @@ -2353,6 +2249,265 @@ pub const Timer = struct { } }; +pub const FFI = struct { + pub const Class = NewClass( + void, + .{ + .name = "FFI", + }, + .{ + .viewSource = .{ + .rfn = JSC.wrapWithHasContainer(JSC.FFI, "print", false, false), + }, + .dlopen = .{ + .rfn = JSC.wrapWithHasContainer(JSC.FFI, "open", false, false), + }, + .ptr = .{ + .rfn = JSC.wrapWithHasContainer(@This(), "ptr", false, false), + }, + .toBuffer = .{ + .rfn = JSC.wrapWithHasContainer(@This(), "toBuffer", false, false), + }, + .toArrayBuffer = .{ + .rfn = JSC.wrapWithHasContainer(@This(), "toArrayBuffer", false, false), + }, + }, + .{ + .CString = .{ + .get = UnsafeCString.getter, + }, + }, + ); + + pub fn ptr( + globalThis: *JSGlobalObject, + value: JSValue, + byteOffset: ?JSValue, + ) JSValue { + if (value.isEmpty()) { + return JSC.JSValue.jsNull(); + } + + const array_buffer = value.asArrayBuffer(globalThis) orelse { + return JSC.toInvalidArguments("Expected ArrayBufferView", .{}, globalThis.ref()); + }; + + if (array_buffer.len == 0) { + return JSC.toInvalidArguments("ArrayBufferView must have a length > 0. A pointer to empty memory doesn't work", .{}, globalThis.ref()); + } + + var addr: usize = @ptrToInt(array_buffer.ptr); + + if (byteOffset) |off| { + if (!off.isEmptyOrUndefinedOrNull()) { + if (!off.isNumber()) { + return JSC.toInvalidArguments("Expected number for byteOffset", .{}, globalThis.ref()); + } + } + + const bytei64 = off.toInt64(); + if (bytei64 < 0) { + addr -|= @intCast(usize, bytei64 * -1); + } else { + addr += @intCast(usize, bytei64); + } + + if (addr > @ptrToInt(array_buffer.ptr) + @as(usize, array_buffer.byte_len)) { + return JSC.toInvalidArguments("byteOffset out of bounds", .{}, globalThis.ref()); + } + } + + if (addr > max_addressible_memory) { + return JSC.toInvalidArguments("Pointer is outside max addressible memory, which usually means a bug in your program.", .{}, globalThis.ref()); + } + + if (addr == 0) { + return JSC.toInvalidArguments("Pointer must not be 0", .{}, globalThis.ref()); + } + + if (addr == 0xDEADBEEF or addr == 0xaaaaaaaa or addr == 0xAAAAAAAA) { + return JSC.toInvalidArguments("ptr to invalid memory, that would segfault Bun :(", .{}, globalThis.ref()); + } + + // truncate to 56 bits to clear any pointer tags + return JSC.JSValue.jsNumber(@bitCast(f64, @as(usize, @truncate(u56, addr)))); + } + + const ValueOrError = union(enum) { + err: JSValue, + slice: []u8, + }; + + pub fn getPtrSlice(globalThis: *JSGlobalObject, value: JSValue, byteOffset: ?JSValue, byteLength: ?JSValue) ValueOrError { + if (!value.isNumber()) { + return .{ .err = JSC.toInvalidArguments("ptr must be a number.", .{}, globalThis.ref()) }; + } + + const num = value.asNumber(); + if (num == 0) { + return .{ .err = JSC.toInvalidArguments("ptr cannot be zero, that would segfault Bun :(", .{}, globalThis.ref()) }; + } + + if (!std.math.isFinite(num)) { + return .{ .err = JSC.toInvalidArguments("ptr must be a finite number.", .{}, globalThis.ref()) }; + } + + var addr = @bitCast(usize, num); + + if (byteOffset) |byte_off| { + if (byte_off.isNumber()) { + const off = byte_off.toInt64(); + if (off < 0) { + addr -|= @intCast(usize, off * -1); + } else { + addr +|= @intCast(usize, off); + } + + if (addr == 0) { + return .{ .err = JSC.toInvalidArguments("ptr cannot be zero, that would segfault Bun :(", .{}, globalThis.ref()) }; + } + + if (!std.math.isFinite(byte_off.asNumber())) { + return .{ .err = JSC.toInvalidArguments("ptr must be a finite number.", .{}, globalThis.ref()) }; + } + } else if (!byte_off.isEmptyOrUndefinedOrNull()) { + // do nothing + } else { + return .{ .err = JSC.toInvalidArguments("Expected number for byteOffset", .{}, globalThis.ref()) }; + } + } + + if (addr == 0xDEADBEEF or addr == 0xaaaaaaaa or addr == 0xAAAAAAAA) { + return .{ .err = JSC.toInvalidArguments("ptr to invalid memory, that would segfault Bun :(", .{}, globalThis.ref()) }; + } + + if (byteLength) |valueLength| { + if (!valueLength.isEmptyOrUndefinedOrNull()) { + if (!valueLength.isNumber()) { + return .{ .err = JSC.toInvalidArguments("length must be a number.", .{}, globalThis.ref()) }; + } + + if (valueLength.asNumber() == 0.0) { + return .{ .err = JSC.toInvalidArguments("length must be > 0. This usually means a bug in your code.", .{}, globalThis.ref()) }; + } + + const length_i = valueLength.toInt64(); + if (length_i < 0) { + return .{ .err = JSC.toInvalidArguments("length must be > 0. This usually means a bug in your code.", .{}, globalThis.ref()) }; + } + + if (length_i > max_addressible_memory) { + return .{ .err = JSC.toInvalidArguments("length exceeds max addressable memory. This usually means a bug in your code.", .{}, globalThis.ref()) }; + } + + const length = @intCast(usize, length_i); + return .{ .slice = @intToPtr([*]u8, addr)[0..length] }; + } + } + + return .{ .slice = std.mem.sliceTo(@intToPtr([*:0]u8, addr), 0) }; + } + + pub fn toArrayBuffer( + globalThis: *JSGlobalObject, + value: JSValue, + byteOffset: ?JSValue, + valueLength: ?JSValue, + ) JSC.JSValue { + switch (getPtrSlice(globalThis, value, byteOffset, valueLength)) { + .err => |erro| { + return erro; + }, + .slice => |slice| { + return JSC.ArrayBuffer.fromBytes(slice, JSC.JSValue.JSType.ArrayBuffer).toJSWithContext(globalThis.ref(), null, null, null); + }, + } + } + + pub fn toBuffer( + globalThis: *JSGlobalObject, + value: JSValue, + byteOffset: ?JSValue, + valueLength: ?JSValue, + ) JSC.JSValue { + switch (getPtrSlice(globalThis, value, byteOffset, valueLength)) { + .err => |erro| { + return erro; + }, + .slice => |slice| { + return JSC.JSValue.createBuffer(globalThis, slice, null); + }, + } + } + + pub fn getter( + _: void, + ctx: js.JSContextRef, + _: js.JSValueRef, + _: js.JSStringRef, + _: js.ExceptionRef, + ) js.JSValueRef { + var existing = ctx.ptr().getCachedObject(&ZigString.init("FFI")); + if (existing.isEmpty()) { + var prototype = JSC.C.JSObjectMake(ctx, FFI.Class.get().?[0], null); + var base = JSC.C.JSObjectMake(ctx, null, null); + JSC.C.JSObjectSetPrototype(ctx, base, prototype); + return ctx.ptr().putCachedObject( + &ZigString.init("FFI"), + JSValue.fromRef(base), + ).asObjectRef(); + } + + return existing.asObjectRef(); + } +}; + +pub const UnsafeCString = struct { + pub fn constructor( + ctx: js.JSContextRef, + _: js.JSObjectRef, + len: usize, + args: [*c]const js.JSValueRef, + exception: js.ExceptionRef, + ) callconv(.C) js.JSObjectRef { + if (len == 0) { + JSC.throwInvalidArguments("Expected a ptr", .{}, ctx, exception); + return null; + } + + return newCString(ctx.ptr(), JSC.JSValue.fromRef(args[0]), if (len > 1) JSC.JSValue.fromRef(args[1]) else null, if (len > 2) JSC.JSValue.fromRef(args[2]) else null).asObjectRef(); + } + + pub fn newCString(globalThis: *JSGlobalObject, value: JSValue, byteOffset: ?JSValue, lengthValue: ?JSValue) JSC.JSValue { + switch (FFI.getPtrSlice(globalThis, value, byteOffset, lengthValue)) { + .err => |err| { + return err; + }, + .slice => |slice| { + return WebCore.Encoder.toString(slice.ptr, slice.len, globalThis, .utf8); + }, + } + } + + pub fn getter( + _: void, + ctx: js.JSContextRef, + _: js.JSValueRef, + _: js.JSStringRef, + _: js.ExceptionRef, + ) js.JSValueRef { + var existing = ctx.ptr().getCachedObject(&ZigString.init("UnsafeCString")); + if (existing.isEmpty()) { + return ctx.ptr().putCachedObject( + &ZigString.init("UnsafeCString"), + JSValue.fromRef(JSC.C.JSObjectMakeConstructor(ctx, null, constructor)), + ).asObjectRef(); + } + + return existing.asObjectRef(); + } +}; + /// EnvironmentVariables is runtime defined. /// Also, you can't iterate over process.env normally since it only exists at build-time otherwise // This is aliased to Bun.env diff --git a/src/javascript/jsc/api/ffi.zig b/src/javascript/jsc/api/ffi.zig index ab866232b..45893eb7d 100644 --- a/src/javascript/jsc/api/ffi.zig +++ b/src/javascript/jsc/api/ffi.zig @@ -98,12 +98,14 @@ pub const FFI = struct { this.closed = true; this.dylib.close(); + const allocator = VirtualMachine.vm.allocator; + for (this.functions.values()) |*val| { - VirtualMachine.vm.allocator.free(bun.constStrToU8(std.mem.span(val.base_name))); + allocator.free(bun.constStrToU8(std.mem.span(val.base_name))); - val.arg_types.deinit(VirtualMachine.vm.allocator); + val.arg_types.deinit(allocator); } - this.functions.deinit(VirtualMachine.vm.allocator); + this.functions.deinit(allocator); return JSC.JSValue.jsUndefined(); } @@ -312,9 +314,28 @@ pub const FFI = struct { try abi_types.ensureTotalCapacityPrecise(allocator, array.len); while (array.next()) |val| { - if (val.isEmptyOrUndefinedOrNull() or !val.jsType().isStringLike()) { + if (val.isEmptyOrUndefinedOrNull()) { + abi_types.clearAndFree(allocator); + return ZigString.init("param must be a string (type name) or number").toErrorInstance(global); + } + + if (val.isAnyInt()) { + const int = val.toInt32(); + switch (int) { + 0...13 => { + abi_types.appendAssumeCapacity(@intToEnum(ABIType, int)); + continue; + }, + else => { + abi_types.clearAndFree(allocator); + return ZigString.init("invalid ABI type").toErrorInstance(global); + }, + } + } + + if (!val.jsType().isStringLike()) { abi_types.clearAndFree(allocator); - return ZigString.init("param must be a string (type name)").toErrorInstance(global); + return ZigString.init("param must be a string (type name) or number").toErrorInstance(global); } var type_name = val.toSlice(global, allocator); @@ -581,39 +602,78 @@ pub const FFI = struct { @"void" = 13, - pub const label = ComptimeStringMap(ABIType, .{ - .{ "bool", .bool }, - .{ "c_int", .int32_t }, - .{ "c_uint", .uint32_t }, - .{ "char", .char }, - .{ "char*", .ptr }, - .{ "double", .double }, - .{ "f32", .float }, - .{ "f64", .double }, - .{ "float", .float }, - .{ "i16", .int16_t }, - .{ "i32", .int32_t }, - .{ "i64", .int64_t }, - .{ "i8", .int8_t }, - .{ "int", .int32_t }, - .{ "int16_t", .int16_t }, - .{ "int32_t", .int32_t }, - .{ "int64_t", .int64_t }, - .{ "int8_t", .int8_t }, - .{ "isize", .int64_t }, - .{ "u16", .uint16_t }, - .{ "u32", .uint32_t }, - .{ "u64", .uint64_t }, - .{ "u8", .uint8_t }, - .{ "uint16_t", .uint16_t }, - .{ "uint32_t", .uint32_t }, - .{ "uint64_t", .uint64_t }, - .{ "uint8_t", .uint8_t }, - .{ "usize", .uint64_t }, - .{ "void*", .ptr }, - .{ "ptr", .ptr }, - .{ "pointer", .ptr }, - }); + const map = .{ + .{ "bool", ABIType.bool }, + .{ "c_int", ABIType.int32_t }, + .{ "c_uint", ABIType.uint32_t }, + .{ "char", ABIType.char }, + .{ "char*", ABIType.ptr }, + .{ "double", ABIType.double }, + .{ "f32", ABIType.float }, + .{ "f64", ABIType.double }, + .{ "float", ABIType.float }, + .{ "i16", ABIType.int16_t }, + .{ "i32", ABIType.int32_t }, + .{ "i64", ABIType.int64_t }, + .{ "i8", ABIType.int8_t }, + .{ "int", ABIType.int32_t }, + .{ "int16_t", ABIType.int16_t }, + .{ "int32_t", ABIType.int32_t }, + .{ "int64_t", ABIType.int64_t }, + .{ "int8_t", ABIType.int8_t }, + .{ "isize", ABIType.int64_t }, + .{ "u16", ABIType.uint16_t }, + .{ "u32", ABIType.uint32_t }, + .{ "u64", ABIType.uint64_t }, + .{ "u8", ABIType.uint8_t }, + .{ "uint16_t", ABIType.uint16_t }, + .{ "uint32_t", ABIType.uint32_t }, + .{ "uint64_t", ABIType.uint64_t }, + .{ "uint8_t", ABIType.uint8_t }, + .{ "usize", ABIType.uint64_t }, + .{ "void*", ABIType.ptr }, + .{ "ptr", ABIType.ptr }, + .{ "pointer", ABIType.ptr }, + }; + pub const label = ComptimeStringMap(ABIType, map); + const EnumMapFormatter = struct { + name: []const u8, + entry: ABIType, + pub fn format(self: EnumMapFormatter, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + try writer.writeAll("['"); + // these are not all valid identifiers + try writer.writeAll(self.name); + try writer.writeAll("']:"); + try std.fmt.formatInt(@enumToInt(self.entry), 10, .lower, .{}, writer); + try writer.writeAll(",'"); + try std.fmt.formatInt(@enumToInt(self.entry), 10, .lower, .{}, writer); + try writer.writeAll("':"); + try std.fmt.formatInt(@enumToInt(self.entry), 10, .lower, .{}, writer); + } + }; + pub const map_to_js_object = brk: { + var count: usize = 2; + for (map) |item, i| { + var fmt = EnumMapFormatter{ .name = item.@"0", .entry = item.@"1" }; + count += std.fmt.count("{}", .{fmt}); + count += @boolToInt(i > 0); + } + + var buf: [count]u8 = undefined; + buf[0] = '{'; + buf[buf.len - 1] = '}'; + var end: usize = 1; + for (map) |item, i| { + var fmt = EnumMapFormatter{ .name = item.@"0", .entry = item.@"1" }; + if (i > 0) { + buf[end] = ','; + end += 1; + } + end += (std.fmt.bufPrint(buf[end..], "{}", .{fmt}) catch unreachable).len; + } + + break :brk buf; + }; pub fn isFloatingPoint(this: ABIType) bool { return switch (this) { diff --git a/src/javascript/jsc/base.zig b/src/javascript/jsc/base.zig index e2d57b9ca..430d60253 100644 --- a/src/javascript/jsc/base.zig +++ b/src/javascript/jsc/base.zig @@ -2882,6 +2882,9 @@ pub fn wrapWithHasContainer( }; args[i] = val; }, + ?JSValue => { + args[i] = iter.protectEatNext(); + }, else => @compileError("Unexpected Type " ++ @typeName(ArgType)), } } @@ -2921,3 +2924,51 @@ pub fn wrapWithHasContainer( } }.callback; } + +pub fn cachedBoundFunction(comptime name: [:0]const u8, comptime callback: anytype) (fn ( + _: void, + ctx: js.JSContextRef, + _: js.JSValueRef, + _: js.JSStringRef, + _: js.ExceptionRef, +) js.JSValueRef) { + return struct { + const name_ = name; + pub fn call( + arg2: js.JSContextRef, + arg3: js.JSObjectRef, + arg4: js.JSObjectRef, + arg5: usize, + arg6: [*c]const js.JSValueRef, + arg7: js.ExceptionRef, + ) callconv(.C) js.JSObjectRef { + return callback( + {}, + arg2, + arg3, + arg4, + arg6[0..arg5], + arg7, + ); + } + + pub fn getter( + _: void, + ctx: js.JSContextRef, + _: js.JSValueRef, + _: js.JSStringRef, + _: js.ExceptionRef, + ) js.JSValueRef { + const name_slice = std.mem.span(name_); + var existing = ctx.ptr().getCachedObject(&ZigString.init(name_slice)); + if (existing.isEmpty()) { + return ctx.ptr().putCachedObject( + &ZigString.init(name_slice), + JSValue.fromRef(JSC.C.JSObjectMakeFunctionWithCallback(ctx, JSC.C.JSStringCreateStatic(name_slice.ptr, name_slice.len), call)), + ).asObjectRef(); + } + + return existing.asObjectRef(); + } + }.getter; +} diff --git a/src/javascript/jsc/ffi.exports.js b/src/javascript/jsc/ffi.exports.js new file mode 100644 index 000000000..59af47d4f --- /dev/null +++ b/src/javascript/jsc/ffi.exports.js @@ -0,0 +1,7 @@ +export const ptr = globalThis.Bun.FFI.ptr; +export const toBuffer = globalThis.Bun.FFI.toBuffer; +export const toArrayBuffer = globalThis.Bun.FFI.toArrayBuffer; +export const CString = globalThis.Bun.FFI.CString; +export const dlopen = globalThis.Bun.FFI.dlopen; +export const viewSource = globalThis.Bun.FFI.viewSource; +// --- FFIType --- diff --git a/src/javascript/jsc/javascript.zig b/src/javascript/jsc/javascript.zig index 009ef160d..5996f5808 100644 --- a/src/javascript/jsc/javascript.zig +++ b/src/javascript/jsc/javascript.zig @@ -1078,6 +1078,14 @@ pub const VirtualMachine = struct { .source_url = ZigString.init("node:path"), .hash = 0, }; + } else if (strings.eqlComptime(_specifier, "bun:ffi")) { + return ResolvedSource{ + .allocator = null, + .source_code = ZigString.init(@embedFile("ffi.exports.js") ++ "export const FFIType = " ++ JSC.FFI.ABIType.map_to_js_object ++ ";\n"), + .specifier = ZigString.init("bun:ffi"), + .source_url = ZigString.init("bun:ffi"), + .hash = 0, + }; } const specifier = normalizeSpecifier(_specifier); @@ -1303,11 +1311,14 @@ pub const VirtualMachine = struct { ret.result = null; ret.path = "node:fs"; return; - } - if (strings.eqlComptime(specifier, "node:path")) { + } else if (strings.eqlComptime(specifier, "node:path")) { ret.result = null; ret.path = "node:path"; return; + } else if (strings.eqlComptime(specifier, "bun:ffi")) { + ret.result = null; + ret.path = "bun:ffi"; + return; } const is_special_source = strings.eqlComptime(source, main_file_name) or js_ast.Macro.isMacroPath(source); diff --git a/src/linker.zig b/src/linker.zig index 9adccff33..55e367dcb 100644 --- a/src/linker.zig +++ b/src/linker.zig @@ -318,12 +318,15 @@ pub const Linker = struct { } } - if (import_record.path.text.len > 4 and strings.eqlComptime(import_record.path.text[0.."bun:".len], "bun:")) { + if (import_record.path.text.len > 4 and strings.eqlComptimeIgnoreLen(import_record.path.text[0.."bun:".len], "bun:")) { import_record.path = Fs.Path.init(import_record.path.text["bun:".len..]); import_record.path.namespace = "bun"; - if (strings.eqlComptime(import_record.path.text, "test")) { + if (strings.eqlComptime(import_record.path.text, "ffi")) { + import_record.path.text = "bun:ffi"; + } else if (strings.eqlComptime(import_record.path.text, "test")) { import_record.tag = .bun_test; } + // don't link bun continue; } diff --git a/types/bun/bun.d.ts b/types/bun/bun.d.ts index da4aa796e..4f68a34f6 100644 --- a/types/bun/bun.d.ts +++ b/types/bun/bun.d.ts @@ -986,6 +986,8 @@ declare module "bun" { */ static readonly byteLength: 32; } + + export const FFI: typeof import("bun:ffi"); } type TypedArray = diff --git a/types/bun/ffi.d.ts b/types/bun/ffi.d.ts new file mode 100644 index 000000000..16a197643 --- /dev/null +++ b/types/bun/ffi.d.ts @@ -0,0 +1,523 @@ +/** + * `bun:ffi` lets you efficiently call C functions & FFI functions from JavaScript + * without writing any C code yourself. + * + * ```js + * import {dlopen, CString, ptr} from 'bun:ffi'; + * + * const lib = dlopen('libsqlite3', {}); + * + * + * ``` + * + * This is powered by just-in-time compiling C wrappers + * that convert JavaScript types to C types and back. Internally, + * bun uses [tinycc](https://github.com/TinyCC/tinycc), so a big thanks + * goes to Fabrice Bellard and TinyCC maintainers for making this possible. + * + */ +declare module "bun:ffi" { + export enum FFIType { + char = 0, + /** + * 8-bit signed integer + * + * Must be a value between -127 and 127 + * + * When passing to a FFI function (C ABI), type coercsion is not performed. + * + * In C: + * ```c + * signed char + * char // on x64 & aarch64 macOS + * ``` + * + * In JavaScript: + * ```js + * var num = 0; + * ``` + */ + int8_t = 1, + /** + * 8-bit signed integer + * + * Must be a value between -127 and 127 + * + * When passing to a FFI function (C ABI), type coercsion is not performed. + * + * In C: + * ```c + * signed char + * char // on x64 & aarch64 macOS + * ``` + * + * In JavaScript: + * ```js + * var num = 0; + * ``` + */ + i8 = 1, + + /** + * 8-bit unsigned integer + * + * Must be a value between 0 and 255 + * + * When passing to a FFI function (C ABI), type coercsion is not performed. + * + * In C: + * ```c + * unsigned char + * ``` + * + * In JavaScript: + * ```js + * var num = 0; + * ``` + */ + uint8_t = 2, + /** + * 8-bit unsigned integer + * + * Must be a value between 0 and 255 + * + * When passing to a FFI function (C ABI), type coercsion is not performed. + * + * In C: + * ```c + * unsigned char + * ``` + * + * In JavaScript: + * ```js + * var num = 0; + * ``` + */ + u8 = 2, + + /** + * 16-bit signed integer + * + * Must be a value between -32768 and 32767 + * + * When passing to a FFI function (C ABI), type coercsion is not performed. + * + * In C: + * ```c + * in16_t + * short // on arm64 & x64 + * ``` + * + * In JavaScript: + * ```js + * var num = 0; + * ``` + */ + int16_t = 3, + /** + * 16-bit signed integer + * + * Must be a value between -32768 and 32767 + * + * When passing to a FFI function (C ABI), type coercsion is not performed. + * + * In C: + * ```c + * in16_t + * short // on arm64 & x64 + * ``` + * + * In JavaScript: + * ```js + * var num = 0; + * ``` + */ + i16 = 3, + + /** + * 16-bit unsigned integer + * + * Must be a value between 0 and 65535, inclusive. + * + * When passing to a FFI function (C ABI), type coercsion is not performed. + * + * In C: + * ```c + * uint16_t + * unsigned short // on arm64 & x64 + * ``` + * + * In JavaScript: + * ```js + * var num = 0; + * ``` + */ + uint16_t = 4, + /** + * 16-bit unsigned integer + * + * Must be a value between 0 and 65535, inclusive. + * + * When passing to a FFI function (C ABI), type coercsion is not performed. + * + * In C: + * ```c + * uint16_t + * unsigned short // on arm64 & x64 + * ``` + * + * In JavaScript: + * ```js + * var num = 0; + * ``` + */ + u16 = 4, + + /** + * 32-bit signed integer + * + */ + int32_t = 5, + + /** + * 32-bit signed integer + * + * Alias of {@link FFIType.int32_t} + */ + i32 = 5, + /** + * 32-bit signed integer + * + * The same as `int` in C + * + * ```c + * int + * ``` + */ + int = 5, + + /** + * 32-bit unsigned integer + * + * The same as `unsigned int` in C (on x64 & arm64) + * + * C: + * ```c + * unsigned int + * ``` + * JavaScript: + * ```js + * ptr(new Uint32Array(1)) + * ``` + */ + uint32_t = 6, + /** + * 32-bit unsigned integer + * + * Alias of {@link FFIType.uint32_t} + */ + u32 = 6, + + /** + * int64 is a 64-bit signed integer + * + * This is not implemented yet! + */ + int64_t = 7, + /** + * i64 is a 64-bit signed integer + * + * This is not implemented yet! + */ + i64 = 7, + + /** + * 64-bit unsigned integer + * + * This is not implemented yet! + */ + uint64_t = 8, + /** + * 64-bit unsigned integer + * + * This is not implemented yet! + */ + u64 = 8, + + /** + * Doubles are not supported yet! + */ + double = 9, + /** + * Doubles are not supported yet! + */ + f64 = 9, + /** + * Floats are not supported yet! + */ + float = 10, + /** + * Floats are not supported yet! + */ + f32 = 10, + + /** + * Booelan value + * + * Must be `true` or `false`. `0` and `1` type coercion is not supported. + * + * In C, this corresponds to: + * ```c + * bool + * _Bool + * ``` + * + * + */ + bool = 11, + + /** + * Pointer value + * + * See {@link Bun.FFI.ptr} for more information + * + * In C: + * ```c + * void* + * ``` + * + * In JavaScript: + * ```js + * ptr(new Uint8Array(1)) + * ``` + */ + ptr = 12, + /** + * Pointer value + * + * alias of {@link FFIType.ptr} + */ + pointer = 12, + + /** + * void value + * + * void arguments are not supported + * + * void return type is the default return type + * + * In C: + * ```c + * void + * ``` + * + */ + void = 13, + } + + type Symbols = Record< + string, + { + /** + * Arguments to a FFI function (C ABI) + * + * Defaults to an empty array, which means no arguments. + * + * To pass a pointer, use "ptr" or "pointer" as the type name. To get a pointer, see {@link ptr}. + * + * @example + * From JavaScript: + * ```js + * const lib = dlopen('add', { + * // FFIType can be used or you can pass string labels. + * args: [FFIType.i32, "i32"], + * return_type: "i32", + * }); + * lib.symbols.add(1, 2) + * ``` + * In C: + * ```c + * int add(int a, int b) { + * return a + b; + * } + * ``` + */ + args?: FFIType[]; + /** + * Return type to a FFI function (C ABI) + * + * Defaults to an empty array, which means no arguments. + * + * To pass a pointer, use "ptr" or "pointer" as the type name. To get a pointer, see {@link ptr}. + * + * @example + * From JavaScript: + * ```js + * const lib = dlopen('add', { + * // FFIType can be used or you can pass string labels. + * args: [FFIType.i32, "i32"], + * return_type: "i32", + * }); + * lib.symbols.add(1, 2) + * ``` + * In C: + * ```c + * int add(int a, int b) { + * return a + b; + * } + * ``` + */ + return_type?: FFIType; + } + >; + + export interface Library { + symbols: Record<string, CallableFunction>; + + /** + * `dlclose` the library, unloading the symbols and freeing memory allocated. + * + * Once called, the library is no longer usable. + * + * Calling a function from a library that has been closed is undefined behavior. + */ + close(): void; + } + + export function dlopen(libraryName: string, symbols: Symbols): Library<T>; + + /** + * Read a pointer as a {@link Buffer} + * + * If `byteLength` is not provided, the pointer is assumed to be 0-terminated. + * + * @param ptr The memory address to read + * @param byteOffset bytes to skip before reading + * @param byteLength bytes to read + * + * While there are some checks to catch invalid pointers, this is a difficult + * thing to do safely. Passing an invalid pointer can crash the program and + * reading beyond the bounds of the pointer will crash the program or cause + * undefined behavior. Use with care! + * + */ + export function toBuffer( + ptr: number, + byteOffset?: number, + byteLength?: number + ): Buffer; + + /** + * Read a pointer as an {@link ArrayBuffer} + * + * If `byteLength` is not provided, the pointer is assumed to be 0-terminated. + * + * @param ptr The memory address to read + * @param byteOffset bytes to skip before reading + * @param byteLength bytes to read + * + * While there are some checks to catch invalid pointers, this is a difficult + * thing to do safely. Passing an invalid pointer can crash the program and + * reading beyond the bounds of the pointer will crash the program or cause + * undefined behavior. Use with care! + */ + export function toArrayBuffer( + ptr: number, + byteOffset?: number, + byteLength?: number + ): ArrayBuffer; + + /** + * Get the pointer backing a {@link TypedArray} or {@link ArrayBuffer} + * + * Use this to pass {@link TypedArray} or {@link ArrayBuffer} to C functions. + * + * This is for use with FFI functions. For performance reasons, FFI will + * not automatically convert typed arrays to C pointers. + * + * @param {TypedArray|ArrayBuffer|DataView} view the typed array or array buffer to get the pointer for + * @param {number} byteOffset optional offset into the view in bytes + * + * @example + * + * From JavaScript: + * ```js + * const array = new Uint8Array(10); + * const rawPtr = ptr(array); + * myCFunction(rawPtr); + * ``` + * To C: + * ```c + * void myCFunction(char* rawPtr) { + * // Do something with rawPtr + * } + * ``` + * + */ + export function ptr( + view: TypedArray | ArrayBufferLike | DataView, + byteOffset?: number + ): number; + + /** + * Get a string from a UTF-8 encoded C string + * If `byteLength` is not provided, the string is assumed to be null-terminated. + * + * @example + * ```js + * var ptr = lib.symbols.getVersion(); + * console.log(new CString(ptr)); + * ``` + * + * @example + * ```js + * var ptr = lib.symbols.getVersion(); + * // print the first 4 characters + * console.log(new CString(ptr, 0, 4)); + * ``` + * + * While there are some checks to catch invalid pointers, this is a difficult + * thing to do safely. Passing an invalid pointer can crash the program and + * reading beyond the bounds of the pointer will crash the program or cause + * undefined behavior. Use with care! + */ + + export class CString extends String { + /** + * Get a string from a UTF-8 encoded C string + * If `byteLength` is not provided, the string is assumed to be null-terminated. + * + * @param ptr The pointer to the C string + * @param byteOffset bytes to skip before reading + * @param byteLength bytes to read + * + * + * @example + * ```js + * var ptr = lib.symbols.getVersion(); + * console.log(new CString(ptr)); + * ``` + * + * @example + * ```js + * var ptr = lib.symbols.getVersion(); + * // print the first 4 characters + * console.log(new CString(ptr, 0, 4)); + * ``` + * + * While there are some checks to catch invalid pointers, this is a difficult + * thing to do safely. Passing an invalid pointer can crash the program and + * reading beyond the bounds of the pointer will crash the program or cause + * undefined behavior. Use with care! + */ + constructor(ptr: number, byteOffset?: number, byteLength?: number): string; + } + + /** + * View the generated C code for FFI bindings + * + * You probably won't need this unless there's a bug in the FFI bindings + * generator or you're just curious. + */ + export function viewSource(symbols: Symbols): string[]; +} diff --git a/types/bun/index.d.ts b/types/bun/index.d.ts index 032c360f4..7cb0c4934 100644 --- a/types/bun/index.d.ts +++ b/types/bun/index.d.ts @@ -5,6 +5,7 @@ /// <reference no-default-lib="true" /> /// <reference lib="esnext" /> /// <reference path="./bun.d.ts" /> +/// <reference path="./ffi.d.ts" /> /// <reference path="./fs.d.ts" /> /// <reference path="./html-rewriter.d.ts" /> /// <reference path="./globals.d.ts" /> diff --git a/types/bun/paths.txt b/types/bun/paths.txt index ffc59bdc7..a7647de9e 100644 --- a/types/bun/paths.txt +++ b/types/bun/paths.txt @@ -1,4 +1,5 @@ ./bun.d.ts +./ffi.d.ts ./fs.d.ts ./html-rewriter.d.ts ./globals.d.ts |