diff --git a/docs/api/cc.md b/docs/api/cc.md new file mode 100644 index 0000000000000..212b928df5404 --- /dev/null +++ b/docs/api/cc.md @@ -0,0 +1,197 @@ +`bun:ffi` has experimental support for compiling and running C from JavaScript with low overhead. + +## Usage (cc in `bun:ffi`) + +See the [introduction blog post](https://bun.sh/blog/compile-and-run-c-in-js) for more information. + +JavaScript: + +```ts#hello.js +import { cc } from "bun:ffi"; +import source from "./hello.c" with { type: "file" }; + +const { + symbols: { hello }, +} = cc({ + source, + symbols: { + hello: { + args: [], + returns: "int", + }, + }, +}); + +console.log("What is the answer to the universe?", hello()); +``` + +C source: + +```c#hello.c +int hello() { + return 42; +} +``` + +When you run `hello.js`, it will print: + +```sh +$ bun hello.js +What is the answer to the universe? 42 +``` + +Under the hood, `cc` uses [TinyCC](https://bellard.org/tcc/) to compile the C code and then link it with the JavaScript runtime, efficiently converting types in-place. + +### Primitive types + +The same `FFIType` values in [`dlopen`](/docs/api/ffi) are supported in `cc`. + +| `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` | | + +### Strings, objects, and non-primitive types + +To make it easier to work with strings, objects, and other non-primitive types that don't map 1:1 to C types, `cc` supports N-API. + +To pass or receive a JavaScript values without any type conversions from a C function, you can use `napi_value`. + +You can also pass a `napi_env` to receive the N-API environment used to call the JavaScript function. + +#### Returning a C string to JavaScript + +For example, if you have a string in C, you can return it to JavaScript like this: + +```ts#hello.js +import { cc } from "bun:ffi"; +import source from "./hello.c" with { type: "file" }; + +const { + symbols: { hello }, +} = cc({ + source, + symbols: { + hello: { + args: ["napi_env"], + returns: "napi_value", + }, + }, +}); + +const result = hello(); +``` + +And in C: + +```c#hello.c +#include + +napi_value hello(napi_env env) { + napi_value result; + napi_create_string_utf8(env, "Hello, Napi!", NAPI_AUTO_LENGTH, &result); + return result; +} +``` + +You can also use this to return other types like objects and arrays: + +```c#hello.c +#include + +napi_value hello(napi_env env) { + napi_value result; + napi_create_object(env, &result); + return result; +} +``` + +### `cc` Reference + +#### `library: string[]` + +The `library` array is used to specify the libraries that should be linked with the C code. + +```ts +type Library = string[]; + +cc({ + source: "hello.c", + library: ["sqlite3"], +}); +``` + +#### `symbols` + +The `symbols` object is used to specify the functions and variables that should be exposed to JavaScript. + +```ts +type Symbols = { + [key: string]: { + args: FFIType[]; + returns: FFIType; + }; +}; +``` + +#### `source` + +The `source` is a file path to the C code that should be compiled and linked with the JavaScript runtime. + +```ts +type Source = string | URL | BunFile; + +cc({ + source: "hello.c", + symbols: { + hello: { + args: [], + returns: "int", + }, + }, +}); +``` + +#### `flags: string | string[]` + +The `flags` is an optional array of strings that should be passed to the TinyCC compiler. + +```ts +type Flags = string | string[]; +``` + +These are flags like `-I` for include directories and `-D` for preprocessor definitions. + +#### `defines: Record` + +The `defines` is an optional object that should be passed to the TinyCC compiler. + +```ts +type Defines = Record; + +cc({ + source: "hello.c", + defines: { + "NDEBUG": "1", + }, +}); +``` + +These are preprocessor definitions passed to the TinyCC compiler. diff --git a/docs/api/ffi.md b/docs/api/ffi.md index 1a276ba0354ce..d3a7fc3e58b80 100644 --- a/docs/api/ffi.md +++ b/docs/api/ffi.md @@ -1,6 +1,6 @@ Use the built-in `bun:ffi` module to efficiently call native libraries from JavaScript. It works with languages that support the C ABI (Zig, Rust, C/C++, C#, Nim, Kotlin, etc). -## Usage (`bun:ffi`) +## dlopen usage (`bun:ffi`) To print the version number of `sqlite3`: @@ -108,25 +108,27 @@ $ zig build-lib add.cpp -dynamic -lc -lc++ The following `FFIType` values are supported. -| `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` | | +| `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` | | ## Strings diff --git a/packages/bun-types/ffi.d.ts b/packages/bun-types/ffi.d.ts index cacb67981dd1f..343548edbe12a 100644 --- a/packages/bun-types/ffi.d.ts +++ b/packages/bun-types/ffi.d.ts @@ -337,6 +337,9 @@ declare module "bun:ffi" { */ u64_fast = 16, function = 17, + + napi_env = 18, + napi_value = 19, } type Pointer = number & { __pointer__: null }; @@ -372,6 +375,8 @@ declare module "bun:ffi" { [FFIType.i64_fast]: number | bigint; [FFIType.u64_fast]: number | bigint; [FFIType.function]: Pointer | JSCallback; // cannot be null + [FFIType.napi_env]: unknown; + [FFIType.napi_value]: unknown; } interface FFITypeToReturnsType { [FFIType.char]: number; @@ -404,6 +409,8 @@ declare module "bun:ffi" { [FFIType.i64_fast]: number | bigint; [FFIType.u64_fast]: number | bigint; [FFIType.function]: Pointer | null; + [FFIType.napi_env]: unknown; + [FFIType.napi_value]: unknown; } interface FFITypeStringToType { ["char"]: FFIType.char; @@ -436,6 +443,8 @@ declare module "bun:ffi" { ["function"]: FFIType.pointer; // for now ["usize"]: FFIType.uint64_t; // for now ["callback"]: FFIType.pointer; // for now + ["napi_env"]: never; + ["napi_value"]: unknown; } type FFITypeOrString = FFIType | keyof FFITypeStringToType; diff --git a/src/bun.js/api/FFI.h b/src/bun.js/api/FFI.h index effe7dc13aed2..6feeaa68bdca5 100644 --- a/src/bun.js/api/FFI.h +++ b/src/bun.js/api/FFI.h @@ -33,6 +33,37 @@ typedef _Bool bool; #define true 1 #define false 0 +#ifndef SRC_JS_NATIVE_API_TYPES_H_ +typedef struct napi_env__ *napi_env; +typedef struct napi_value__ *napi_value; +typedef enum { + napi_ok, + napi_invalid_arg, + napi_object_expected, + napi_string_expected, + napi_name_expected, + napi_function_expected, + napi_number_expected, + napi_boolean_expected, + napi_array_expected, + napi_generic_failure, + napi_pending_exception, + napi_cancelled, + napi_escape_called_twice, + napi_handle_scope_mismatch, + napi_callback_scope_mismatch, + napi_queue_full, + napi_closing, + napi_bigint_expected, + napi_date_expected, + napi_arraybuffer_expected, + napi_detachable_arraybuffer_expected, + napi_would_deadlock // unused +} napi_status; +void* NapiHandleScope__push(void* jsGlobalObject, bool detached); +void NapiHandleScope__pop(void* jsGlobalObject, void* handleScope); +#endif + #ifdef INJECT_BEFORE // #include @@ -68,6 +99,8 @@ typedef union EncodedJSValue { JSCell *ptr; #endif +napi_value asNapiValue; + #if IS_BIG_ENDIAN struct { int32_t tag; diff --git a/src/bun.js/api/ffi.zig b/src/bun.js/api/ffi.zig index a5ad1f9aee31f..6524f10526c42 100644 --- a/src/bun.js/api/ffi.zig +++ b/src/bun.js/api/ffi.zig @@ -609,7 +609,7 @@ pub const FFI = struct { } } - const symbols_object = object.get(globalThis, "symbols") orelse .zero; + const symbols_object = object.get(globalThis, "symbols") orelse .undefined; if (!globalThis.hasException() and (symbols_object == .zero or !symbols_object.isObject())) { _ = globalThis.throwInvalidArgumentTypeValue("symbols", "object", symbols_object); } @@ -763,9 +763,9 @@ pub const FFI = struct { if (tcc_state) |state| { TCC.tcc_delete(state); } - if (bytes_to_free_on_error.len > 0) { - bun.default_allocator.destroy(@as(*u8, @ptrCast(bytes_to_free_on_error))); - } + + // TODO: upgrade tinycc because they improved the way memory management works for this + // we are unable to free memory safely in certain cases here. } var obj = JSC.JSValue.createEmptyObject(globalThis, compile_c.symbols.map.count()); @@ -774,11 +774,14 @@ pub const FFI = struct { const allocator = bun.default_allocator; function.compile(allocator) catch |err| { - const ret = JSC.toInvalidArguments("{s} when translating symbol \"{s}\"", .{ - @errorName(err), - function_name, - }, globalThis); - globalThis.throwValue(ret); + if (!globalThis.hasException()) { + const ret = JSC.toInvalidArguments("{s} when translating symbol \"{s}\"", .{ + @errorName(err), + function_name, + }, globalThis); + globalThis.throwValue(ret); + } + return .zero; }; switch (function.step) { @@ -1326,11 +1329,11 @@ pub const FFI = struct { var threadsafe = false; - if (value.get(global, "threadsafe")) |threadsafe_value| { + if (value.getTruthy(global, "threadsafe")) |threadsafe_value| { threadsafe = threadsafe_value.toBoolean(); } - if (value.get(global, "returns")) |ret_value| brk: { + if (value.getTruthy(global, "returns")) |ret_value| brk: { if (ret_value.isAnyInt()) { const int = ret_value.toInt32(); switch (int) { @@ -1353,6 +1356,11 @@ pub const FFI = struct { }; } + if (return_type == ABIType.napi_env) { + abi_types.clearAndFree(allocator); + return ZigString.static("Cannot return napi_env to JavaScript").toErrorInstance(global); + } + if (function.threadsafe and return_type != ABIType.void) { abi_types.clearAndFree(allocator); return ZigString.static("Threadsafe functions must return void").toErrorInstance(global); @@ -1424,6 +1432,15 @@ pub const FFI = struct { pub var lib_dirZ: [*:0]const u8 = ""; + pub fn needsHandleScope(val: *const Function) bool { + for (val.arg_types.items) |arg| { + if (arg == ABIType.napi_env or arg == ABIType.napi_value) { + return true; + } + } + return val.return_type == ABIType.napi_value; + } + extern "C" fn FFICallbackFunctionWrapper_destroy(*anyopaque) void; pub fn deinit(val: *Function, globalThis: *JSC.JSGlobalObject, allocator: std.mem.Allocator) void { @@ -1765,22 +1782,46 @@ pub const FFI = struct { \\ ); + if (this.needsHandleScope()) { + try writer.writeAll( + \\ void* handleScope = NapiHandleScope__push(JS_GLOBAL_OBJECT, false); + \\ + ); + } + if (this.arg_types.items.len > 0) { try writer.writeAll( \\ LOAD_ARGUMENTS_FROM_CALL_FRAME; \\ ); for (this.arg_types.items, 0..) |arg, i| { - if (arg.needsACastInC()) { + if (arg == .napi_env) { + try writer.print( + \\ napi_env arg{d} = (napi_env)JS_GLOBAL_OBJECT; + \\ argsPtr++; + \\ + , + .{ + i, + }, + ); + } else if (arg == .napi_value) { + try writer.print( + \\ EncodedJSValue arg{d} = {{ .asInt64 = *argsPtr++ }}; + \\ + , + .{ + i, + }, + ); + } else if (arg.needsACastInC()) { if (i < this.arg_types.items.len - 1) { try writer.print( - \\ EncodedJSValue arg{d}; - \\ arg{d}.asInt64 = *argsPtr++; + \\ EncodedJSValue arg{d} = {{ .asInt64 = *argsPtr++ }}; \\ , .{ i, - i, }, ); } else { @@ -1854,6 +1895,13 @@ pub const FFI = struct { try writer.writeAll(" "); + if (this.needsHandleScope()) { + try writer.writeAll( + \\ NapiHandleScope__pop(JS_GLOBAL_OBJECT, handleScope); + \\ + ); + } + try writer.writeAll("return "); if (!(this.return_type == .void)) { @@ -2014,8 +2062,10 @@ pub const FFI = struct { u64_fast = 16, function = 17, + napi_env = 18, + napi_value = 19, - pub const max = @intFromEnum(ABIType.function); + pub const max = @intFromEnum(ABIType.napi_value); /// Types that we can directly pass through as an `int64_t` pub fn needsACastInC(this: ABIType) bool { @@ -2064,6 +2114,8 @@ pub const FFI = struct { .{ "function", ABIType.function }, .{ "callback", ABIType.function }, .{ "fn", ABIType.function }, + .{ "napi_env", ABIType.napi_env }, + .{ "napi_value", ABIType.napi_value }, }; pub const label = bun.ComptimeStringMap(ABIType, map); const EnumMapFormatter = struct { @@ -2158,6 +2210,15 @@ pub const FFI = struct { try writer.writeAll("(float)"); try writer.writeAll("JSVALUE_TO_FLOAT("); }, + .napi_env => { + try writer.writeAll("(napi_env)JS_GLOBAL_OBJECT"); + return; + }, + .napi_value => { + try writer.writeAll(self.symbol); + try writer.writeAll(".asNapiValue"); + return; + }, } // if (self.fromi64) { // try writer.writeAll("EncodedJSValue{ "); @@ -2207,6 +2268,12 @@ pub const FFI = struct { .float => { try writer.print("FLOAT_TO_JSVALUE({s})", .{self.symbol}); }, + .napi_env => { + try writer.writeAll("JS_GLOBAL_OBJECT"); + }, + .napi_value => { + try writer.print("((EncodedJSValue) {{.asNapiValue = {s} }} )", .{self.symbol}); + }, } } }; @@ -2249,6 +2316,8 @@ pub const FFI = struct { .float => "float", .char => "char", .void => "void", + .napi_env => "napi_env", + .napi_value => "napi_value", }; } @@ -2274,6 +2343,8 @@ pub const FFI = struct { .float => "float", .char => "char", .void => "void", + .napi_env => "napi_env", + .napi_value => "napi_value", }; } }; @@ -2357,6 +2428,8 @@ const CompilerRT = struct { pub fn inject(state: *TCC.TCCState) void { _ = TCC.tcc_add_symbol(state, "memset", &memset); _ = TCC.tcc_add_symbol(state, "memcpy", &memcpy); + _ = TCC.tcc_add_symbol(state, "NapiHandleScope__push", &bun.JSC.napi.NapiHandleScope.NapiHandleScope__push); + _ = TCC.tcc_add_symbol(state, "NapiHandleScope__pop", &bun.JSC.napi.NapiHandleScope.NapiHandleScope__pop); _ = TCC.tcc_add_symbol( state, diff --git a/src/js/bun/ffi.ts b/src/js/bun/ffi.ts index 886e0f8903d6f..f6978c0966f9b 100644 --- a/src/js/bun/ffi.ts +++ b/src/js/bun/ffi.ts @@ -55,6 +55,8 @@ const FFIType = { function: 17, callback: 17, fn: 17, + napi_env: 18, + napi_value: 19, }; const suffix = process.platform === "win32" ? "dll" : process.platform === "darwin" ? "dylib" : "so"; @@ -153,7 +155,7 @@ Object.defineProperty(globalThis, "__GlobalBunCString", { configurable: false, }); -const ffiWrappers = new Array(18); +const ffiWrappers = new Array(20); var char = "val|0"; ffiWrappers.fill(char); diff --git a/src/napi/napi.zig b/src/napi/napi.zig index 1573de55a7bb6..96d1fe096da92 100644 --- a/src/napi/napi.zig +++ b/src/napi/napi.zig @@ -65,8 +65,8 @@ pub const Ref = opaque { extern fn napi_set_ref(ref: *Ref, value: JSC.JSValue) void; }; pub const NapiHandleScope = opaque { - extern fn NapiHandleScope__push(globalObject: *JSC.JSGlobalObject, escapable: bool) ?*NapiHandleScope; - extern fn NapiHandleScope__pop(globalObject: *JSC.JSGlobalObject, current: ?*NapiHandleScope) void; + pub extern fn NapiHandleScope__push(globalObject: *JSC.JSGlobalObject, escapable: bool) ?*NapiHandleScope; + pub extern fn NapiHandleScope__pop(globalObject: *JSC.JSGlobalObject, current: ?*NapiHandleScope) void; extern fn NapiHandleScope__append(globalObject: *JSC.JSGlobalObject, value: JSC.JSValueReprInt) void; extern fn NapiHandleScope__escape(handleScope: *NapiHandleScope, value: JSC.JSValueReprInt) bool; diff --git a/test/js/bun/ffi/cc-fixture.c b/test/js/bun/ffi/cc-fixture.c index face36e331576..e39a15c784796 100644 --- a/test/js/bun/ffi/cc-fixture.c +++ b/test/js/bun/ffi/cc-fixture.c @@ -8,6 +8,18 @@ #include #endif +#if __has_include() + +#include + +napi_value napi_main(napi_env env) { + napi_value result; + napi_create_string_utf8(env, "Hello, Napi!", NAPI_AUTO_LENGTH, &result); + return result; +} + +#endif + int main() { #if __has_include() diff --git a/test/js/bun/ffi/cc-fixture.js b/test/js/bun/ffi/cc-fixture.js index 212dfdaca96d6..b3d4d642c4f72 100644 --- a/test/js/bun/ffi/cc-fixture.js +++ b/test/js/bun/ffi/cc-fixture.js @@ -1,14 +1,18 @@ import { cc } from "bun:ffi"; import fixture from "./cc-fixture.c" with { type: "file" }; const { - symbols: { main }, + symbols: { napi_main, main }, } = cc({ source: fixture, define: { "HAS_MY_DEFINE": '"my value"', }, - flags: ["-l/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation"], + symbols: { + "napi_main": { + args: ["napi_env"], + returns: "napi_value", + }, "main": { args: [], returns: "int", @@ -19,3 +23,7 @@ const { if (main() !== 42) { throw new Error("main() !== 42"); } + +if (napi_main(null) !== "Hello, Napi!") { + throw new Error("napi_main() !== Hello, Napi!"); +}