diff --git a/packages/bun-types/ffi.d.ts b/packages/bun-types/ffi.d.ts index 565753c63d524..e222c31244b4b 100644 --- a/packages/bun-types/ffi.d.ts +++ b/packages/bun-types/ffi.d.ts @@ -597,6 +597,66 @@ declare module "bun:ffi" { symbols: Fns, ): Library; + /** + * **Experimental:** Compile ISO C11 source code using TinyCC, and make {@link symbols} available as functions to JavaScript. + * + * @param options + * @returns Library + * + * @example + * ## Hello, World! + * + * JavaScript: + * ```js + * import { cc } from "bun:ffi"; + * import hello from "./hello.c" with {type: "file"}; + * const {symbols: {hello}} = cc({ + * source: hello, + * symbols: { + * hello: { + * returns: "cstring", + * args: [], + * }, + * }, + * }); + * // "Hello, World!" + * console.log(hello()); + * ``` + * + * `./hello.c`: + * ```c + * #include + * const char* hello() { + * return "Hello, World!"; + * } + * ``` + */ + function cc>(options: { + /** + * File path to an ISO C11 source file to compile and link + */ + source: string | import("bun").BunFile | URL; + + /** + * Library names to link against + * + * Equivalent to `-l` option in gcc/clang. + */ + library?: string[] | string; + + /** + * Include directories to pass to the compiler + * + * Equivalent to `-I` option in gcc/clang. + */ + include?: string[] | string; + + /** + * Map of symbols to load where the key is the symbol name and the value is the {@link FFIFunction} + */ + symbols: Fns; + }): Library; + /** * Turn a native library's function pointer into a JavaScript function * diff --git a/src/bun.js/api/ffi-stdalign.h b/src/bun.js/api/ffi-stdalign.h new file mode 100644 index 0000000000000..03c59b5dca472 --- /dev/null +++ b/src/bun.js/api/ffi-stdalign.h @@ -0,0 +1,15 @@ +#ifndef _STDALIGN_H +#define _STDALIGN_H + +#if __STDC_VERSION__ < 201112L && (defined(__GNUC__) || defined(__TINYC__)) +#define _Alignas(t) __attribute__((__aligned__(t))) +#define _Alignof(t) __alignof__(t) +#endif + +#define alignas _Alignas +#define alignof _Alignof + +#define __alignas_is_defined 1 +#define __alignof_is_defined 1 + +#endif /* _STDALIGN_H */ diff --git a/src/bun.js/api/ffi-stdarg.h b/src/bun.js/api/ffi-stdarg.h new file mode 100644 index 0000000000000..aa784da243026 --- /dev/null +++ b/src/bun.js/api/ffi-stdarg.h @@ -0,0 +1,14 @@ +#ifndef _STDARG_H +#define _STDARG_H + +typedef __builtin_va_list va_list; +#define va_start __builtin_va_start +#define va_arg __builtin_va_arg +#define va_copy __builtin_va_copy +#define va_end __builtin_va_end + +/* fix a buggy dependency on GCC in libio.h */ +typedef va_list __gnuc_va_list; +#define _VA_LIST_DEFINED + +#endif /* _STDARG_H */ diff --git a/src/bun.js/api/ffi-stdatomic.h b/src/bun.js/api/ffi-stdatomic.h new file mode 100644 index 0000000000000..7ac662816be1e --- /dev/null +++ b/src/bun.js/api/ffi-stdatomic.h @@ -0,0 +1,180 @@ +/* This file is derived from clang's stdatomic.h */ + +/*===---- stdatomic.h - Standard header for atomic types and operations -----=== + * + * Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. + * See https://llvm.org/LICENSE.txt for license information. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + * + *===-----------------------------------------------------------------------=== + */ + +#ifndef _STDATOMIC_H +#define _STDATOMIC_H + +#include +#include +#include + +#define __ATOMIC_RELAXED 0 +#define __ATOMIC_CONSUME 1 +#define __ATOMIC_ACQUIRE 2 +#define __ATOMIC_RELEASE 3 +#define __ATOMIC_ACQ_REL 4 +#define __ATOMIC_SEQ_CST 5 + +/* Memory ordering */ +typedef enum { + memory_order_relaxed = __ATOMIC_RELAXED, + memory_order_consume = __ATOMIC_CONSUME, + memory_order_acquire = __ATOMIC_ACQUIRE, + memory_order_release = __ATOMIC_RELEASE, + memory_order_acq_rel = __ATOMIC_ACQ_REL, + memory_order_seq_cst = __ATOMIC_SEQ_CST, +} memory_order; + +/* Atomic typedefs */ +typedef _Atomic(_Bool) atomic_bool; +typedef _Atomic(char) atomic_char; +typedef _Atomic(signed char) atomic_schar; +typedef _Atomic(unsigned char) atomic_uchar; +typedef _Atomic(short) atomic_short; +typedef _Atomic(unsigned short) atomic_ushort; +typedef _Atomic(int) atomic_int; +typedef _Atomic(unsigned int) atomic_uint; +typedef _Atomic(long) atomic_long; +typedef _Atomic(unsigned long) atomic_ulong; +typedef _Atomic(long long) atomic_llong; +typedef _Atomic(unsigned long long) atomic_ullong; +typedef _Atomic(uint_least16_t) atomic_char16_t; +typedef _Atomic(uint_least32_t) atomic_char32_t; +typedef _Atomic(wchar_t) atomic_wchar_t; +typedef _Atomic(int_least8_t) atomic_int_least8_t; +typedef _Atomic(uint_least8_t) atomic_uint_least8_t; +typedef _Atomic(int_least16_t) atomic_int_least16_t; +typedef _Atomic(uint_least16_t) atomic_uint_least16_t; +typedef _Atomic(int_least32_t) atomic_int_least32_t; +typedef _Atomic(uint_least32_t) atomic_uint_least32_t; +typedef _Atomic(int_least64_t) atomic_int_least64_t; +typedef _Atomic(uint_least64_t) atomic_uint_least64_t; +typedef _Atomic(int_fast8_t) atomic_int_fast8_t; +typedef _Atomic(uint_fast8_t) atomic_uint_fast8_t; +typedef _Atomic(int_fast16_t) atomic_int_fast16_t; +typedef _Atomic(uint_fast16_t) atomic_uint_fast16_t; +typedef _Atomic(int_fast32_t) atomic_int_fast32_t; +typedef _Atomic(uint_fast32_t) atomic_uint_fast32_t; +typedef _Atomic(int_fast64_t) atomic_int_fast64_t; +typedef _Atomic(uint_fast64_t) atomic_uint_fast64_t; +typedef _Atomic(intptr_t) atomic_intptr_t; +typedef _Atomic(uintptr_t) atomic_uintptr_t; +typedef _Atomic(size_t) atomic_size_t; +typedef _Atomic(ptrdiff_t) atomic_ptrdiff_t; +typedef _Atomic(intmax_t) atomic_intmax_t; +typedef _Atomic(uintmax_t) atomic_uintmax_t; + +/* Atomic flag */ +typedef struct { + atomic_bool value; +} atomic_flag; + +#define ATOMIC_FLAG_INIT {0} +#define ATOMIC_VAR_INIT(value) (value) + +#define atomic_flag_test_and_set_explicit(object, order) \ + __atomic_test_and_set((void *)(&((object)->value)), order) +#define atomic_flag_test_and_set(object) \ + atomic_flag_test_and_set_explicit(object, __ATOMIC_SEQ_CST) + +#define atomic_flag_clear_explicit(object, order) \ + __atomic_clear((bool *)(&((object)->value)), order) +#define atomic_flag_clear(object) \ + atomic_flag_clear_explicit(object, __ATOMIC_SEQ_CST) + +/* Generic routines */ +#define atomic_init(object, desired) \ + atomic_store_explicit(object, desired, __ATOMIC_RELAXED) + +#define atomic_store_explicit(object, desired, order) \ + ({ \ + __typeof__(object) ptr = (object); \ + __typeof__(*ptr) tmp = (desired); \ + __atomic_store(ptr, &tmp, (order)); \ + }) +#define atomic_store(object, desired) \ + atomic_store_explicit(object, desired, __ATOMIC_SEQ_CST) + +#define atomic_load_explicit(object, order) \ + ({ \ + __typeof__(object) ptr = (object); \ + __typeof__(*ptr) tmp; \ + __atomic_load(ptr, &tmp, (order)); \ + tmp; \ + }) +#define atomic_load(object) atomic_load_explicit(object, __ATOMIC_SEQ_CST) + +#define atomic_exchange_explicit(object, desired, order) \ + ({ \ + __typeof__(object) ptr = (object); \ + __typeof__(*ptr) val = (desired); \ + __typeof__(*ptr) tmp; \ + __atomic_exchange(ptr, &val, &tmp, (order)); \ + tmp; \ + }) +#define atomic_exchange(object, desired) \ + atomic_exchange_explicit(object, desired, __ATOMIC_SEQ_CST) + +#define atomic_compare_exchange_strong_explicit(object, expected, desired, \ + success, failure) \ + ({ \ + __typeof__(object) ptr = (object); \ + __typeof__(*ptr) tmp = desired; \ + __atomic_compare_exchange(ptr, expected, &tmp, 0, success, failure); \ + }) +#define atomic_compare_exchange_strong(object, expected, desired) \ + atomic_compare_exchange_strong_explicit(object, expected, desired, \ + __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST) + +#define atomic_compare_exchange_weak_explicit(object, expected, desired, \ + success, failure) \ + ({ \ + __typeof__(object) ptr = (object); \ + __typeof__(*ptr) tmp = desired; \ + __atomic_compare_exchange(ptr, expected, &tmp, 1, success, failure); \ + }) +#define atomic_compare_exchange_weak(object, expected, desired) \ + atomic_compare_exchange_weak_explicit(object, expected, desired, \ + __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST) + +#define atomic_fetch_add(object, operand) \ + __atomic_fetch_add(object, operand, __ATOMIC_SEQ_CST) +#define atomic_fetch_add_explicit __atomic_fetch_add + +#define atomic_fetch_sub(object, operand) \ + __atomic_fetch_sub(object, operand, __ATOMIC_SEQ_CST) +#define atomic_fetch_sub_explicit __atomic_fetch_sub + +#define atomic_fetch_or(object, operand) \ + __atomic_fetch_or(object, operand, __ATOMIC_SEQ_CST) +#define atomic_fetch_or_explicit __atomic_fetch_or + +#define atomic_fetch_xor(object, operand) \ + __atomic_fetch_xor(object, operand, __ATOMIC_SEQ_CST) +#define atomic_fetch_xor_explicit __atomic_fetch_xor + +#define atomic_fetch_and(object, operand) \ + __atomic_fetch_and(object, operand, __ATOMIC_SEQ_CST) +#define atomic_fetch_and_explicit __atomic_fetch_and + +extern void atomic_thread_fence(memory_order); +extern void __atomic_thread_fence(memory_order); +#define atomic_thread_fence(order) __atomic_thread_fence(order) +extern void atomic_signal_fence(memory_order); +extern void __atomic_signal_fence(memory_order); +#define atomic_signal_fence(order) __atomic_signal_fence(order) +extern bool __atomic_is_lock_free(size_t size, void *ptr); +#define atomic_is_lock_free(OBJ) __atomic_is_lock_free(sizeof(*(OBJ)), (OBJ)) + +extern bool __atomic_test_and_set(void *, memory_order); +extern void __atomic_clear(bool *, memory_order); + +#endif /* _STDATOMIC_H */ diff --git a/src/bun.js/api/ffi-stdbool.h b/src/bun.js/api/ffi-stdbool.h new file mode 100644 index 0000000000000..89b1643237489 --- /dev/null +++ b/src/bun.js/api/ffi-stdbool.h @@ -0,0 +1,11 @@ +#ifndef _STDBOOL_H +#define _STDBOOL_H + +/* ISOC99 boolean */ + +#define bool _Bool +#define true 1 +#define false 0 +#define __bool_true_false_are_defined 1 + +#endif /* _STDBOOL_H */ diff --git a/src/bun.js/api/ffi-stddef.h b/src/bun.js/api/ffi-stddef.h new file mode 100644 index 0000000000000..6356a5c810039 --- /dev/null +++ b/src/bun.js/api/ffi-stddef.h @@ -0,0 +1,44 @@ +#ifndef _STDDEF_H +#define _STDDEF_H + +typedef __SIZE_TYPE__ size_t; +typedef __PTRDIFF_TYPE__ ssize_t; +typedef __WCHAR_TYPE__ wchar_t; +typedef __PTRDIFF_TYPE__ ptrdiff_t; +typedef __PTRDIFF_TYPE__ intptr_t; +typedef __SIZE_TYPE__ uintptr_t; + +#if __STDC_VERSION__ >= 201112L +typedef union { + long long __ll; + long double __ld; +} max_align_t; +#endif + +#ifndef NULL +#define NULL ((void *)0) +#endif + +#undef offsetof +#define offsetof(type, field) ((size_t) & ((type *)0)->field) + +#if defined __i386__ || defined __x86_64__ +void *alloca(size_t size); +#endif + +#endif + +/* Older glibc require a wint_t from (when requested + by __need_wint_t, as otherwise stddef.h isn't allowed to + define this type). Note that this must be outside the normal + _STDDEF_H guard, so that it works even when we've included the file + already (without requiring wint_t). Some other libs define _WINT_T + if they've already provided that type, so we can use that as guard. + TCC defines __WINT_TYPE__ for us. */ +#if defined(__need_wint_t) +#ifndef _WINT_T +#define _WINT_T +typedef __WINT_TYPE__ wint_t; +#endif +#undef __need_wint_t +#endif diff --git a/src/bun.js/api/ffi-stdnoreturn.h b/src/bun.js/api/ffi-stdnoreturn.h new file mode 100644 index 0000000000000..4d580ea5b85c1 --- /dev/null +++ b/src/bun.js/api/ffi-stdnoreturn.h @@ -0,0 +1,7 @@ +#ifndef _STDNORETURN_H +#define _STDNORETURN_H + +/* ISOC11 noreturn */ +#define noreturn _Noreturn + +#endif /* _STDNORETURN_H */ diff --git a/src/bun.js/api/ffi-tgmath.h b/src/bun.js/api/ffi-tgmath.h new file mode 100644 index 0000000000000..85d81222cb028 --- /dev/null +++ b/src/bun.js/api/ffi-tgmath.h @@ -0,0 +1,89 @@ +/* + * ISO C Standard: 7.22 Type-generic math + */ + +#ifndef _TGMATH_H +#define _TGMATH_H + +#include + +#ifndef __cplusplus +#define __tgmath_real(x, F) \ + _Generic((x), float: F##f, long double: F##l, default: F)(x) +#define __tgmath_real_2_1(x, y, F) \ + _Generic((x), float: F##f, long double: F##l, default: F)(x, y) +#define __tgmath_real_2(x, y, F) \ + _Generic((x) + (y), float: F##f, long double: F##l, default: F)(x, y) +#define __tgmath_real_3_2(x, y, z, F) \ + _Generic((x) + (y), float: F##f, long double: F##l, default: F)(x, y, z) +#define __tgmath_real_3(x, y, z, F) \ + _Generic((x) + (y) + (z), float: F##f, long double: F##l, default: F)(x, y, z) + +/* Functions defined in both and (7.22p4) */ +#define acos(z) __tgmath_real(z, acos) +#define asin(z) __tgmath_real(z, asin) +#define atan(z) __tgmath_real(z, atan) +#define acosh(z) __tgmath_real(z, acosh) +#define asinh(z) __tgmath_real(z, asinh) +#define atanh(z) __tgmath_real(z, atanh) +#define cos(z) __tgmath_real(z, cos) +#define sin(z) __tgmath_real(z, sin) +#define tan(z) __tgmath_real(z, tan) +#define cosh(z) __tgmath_real(z, cosh) +#define sinh(z) __tgmath_real(z, sinh) +#define tanh(z) __tgmath_real(z, tanh) +#define exp(z) __tgmath_real(z, exp) +#define log(z) __tgmath_real(z, log) +#define pow(z1, z2) __tgmath_real_2(z1, z2, pow) +#define sqrt(z) __tgmath_real(z, sqrt) +#define fabs(z) __tgmath_real(z, fabs) + +/* Functions defined in only (7.22p5) */ +#define atan2(x, y) __tgmath_real_2(x, y, atan2) +#define cbrt(x) __tgmath_real(x, cbrt) +#define ceil(x) __tgmath_real(x, ceil) +#define copysign(x, y) __tgmath_real_2(x, y, copysign) +#define erf(x) __tgmath_real(x, erf) +#define erfc(x) __tgmath_real(x, erfc) +#define exp2(x) __tgmath_real(x, exp2) +#define expm1(x) __tgmath_real(x, expm1) +#define fdim(x, y) __tgmath_real_2(x, y, fdim) +#define floor(x) __tgmath_real(x, floor) +#define fma(x, y, z) __tgmath_real_3(x, y, z, fma) +#define fmax(x, y) __tgmath_real_2(x, y, fmax) +#define fmin(x, y) __tgmath_real_2(x, y, fmin) +#define fmod(x, y) __tgmath_real_2(x, y, fmod) +#define frexp(x, y) __tgmath_real_2_1(x, y, frexp) +#define hypot(x, y) __tgmath_real_2(x, y, hypot) +#define ilogb(x) __tgmath_real(x, ilogb) +#define ldexp(x, y) __tgmath_real_2_1(x, y, ldexp) +#define lgamma(x) __tgmath_real(x, lgamma) +#define llrint(x) __tgmath_real(x, llrint) +#define llround(x) __tgmath_real(x, llround) +#define log10(x) __tgmath_real(x, log10) +#define log1p(x) __tgmath_real(x, log1p) +#define log2(x) __tgmath_real(x, log2) +#define logb(x) __tgmath_real(x, logb) +#define lrint(x) __tgmath_real(x, lrint) +#define lround(x) __tgmath_real(x, lround) +#define nearbyint(x) __tgmath_real(x, nearbyint) +#define nextafter(x, y) __tgmath_real_2(x, y, nextafter) +#define nexttoward(x, y) __tgmath_real_2(x, y, nexttoward) +#define remainder(x, y) __tgmath_real_2(x, y, remainder) +#define remquo(x, y, z) __tgmath_real_3_2(x, y, z, remquo) +#define rint(x) __tgmath_real(x, rint) +#define round(x) __tgmath_real(x, round) +#define scalbln(x, y) __tgmath_real_2_1(x, y, scalbln) +#define scalbn(x, y) __tgmath_real_2_1(x, y, scalbn) +#define tgamma(x) __tgmath_real(x, tgamma) +#define trunc(x) __tgmath_real(x, trunc) + +/* Functions defined in only (7.22p6) +#define carg(z) __tgmath_cplx_only(z, carg) +#define cimag(z) __tgmath_cplx_only(z, cimag) +#define conj(z) __tgmath_cplx_only(z, conj) +#define cproj(z) __tgmath_cplx_only(z, cproj) +#define creal(z) __tgmath_cplx_only(z, creal) +*/ +#endif /* __cplusplus */ +#endif /* _TGMATH_H */ diff --git a/src/bun.js/api/ffi.zig b/src/bun.js/api/ffi.zig index a21f2a3482343..37303dcc1d597 100644 --- a/src/bun.js/api/ffi.zig +++ b/src/bun.js/api/ffi.zig @@ -8,6 +8,7 @@ const Global = bun.Global; const strings = bun.strings; const string = bun.string; const Output = bun.Output; +const debug = Output.scoped(.TCC, false); const MutableString = bun.MutableString; const std = @import("std"); const Allocator = std.mem.Allocator; @@ -73,16 +74,545 @@ const VirtualMachine = JSC.VirtualMachine; const IOTask = JSC.IOTask; const TCC = @import("../../tcc.zig"); +extern fn pthread_jit_write_protect_np(enable: bool) callconv(.C) void; pub const FFI = struct { dylib: ?std.DynLib = null, + relocated_bytes_to_free: ?[]u8 = null, functions: bun.StringArrayHashMapUnmanaged(Function) = .{}, closed: bool = false, + shared_state: ?*TCC.TCCState = null, pub usingnamespace JSC.Codegen.JSFFI; pub fn finalize(_: *FFI) callconv(.C) void {} + const CompileC = struct { + source_file: [:0]const u8 = "", + + libraries: std.ArrayListUnmanaged([:0]const u8) = .{}, + library_dirs: std.ArrayListUnmanaged([:0]const u8) = .{}, + include_dirs: std.ArrayListUnmanaged([:0]const u8) = .{}, + symbols: SymbolsMap = .{}, + + deferred_errors: std.ArrayListUnmanaged([]const u8) = .{}, + + const stdarg = struct { + extern "C" fn ffi_vfprintf(*anyopaque, [*:0]const u8, ...) callconv(.C) c_int; + extern "C" fn ffi_vprintf([*:0]const u8, ...) callconv(.C) c_int; + extern "C" fn ffi_fprintf(*anyopaque, [*:0]const u8, ...) callconv(.C) c_int; + extern "C" fn ffi_printf([*:0]const u8, ...) callconv(.C) c_int; + extern "C" fn ffi_fscanf(*anyopaque, [*:0]const u8, ...) callconv(.C) c_int; + extern "C" fn ffi_scanf([*:0]const u8, ...) callconv(.C) c_int; + extern "C" fn ffi_sscanf([*:0]const u8, [*:0]const u8, ...) callconv(.C) c_int; + extern "C" fn ffi_vsscanf([*:0]const u8, [*:0]const u8, ...) callconv(.C) c_int; + extern "C" fn ffi_fopen([*:0]const u8, [*:0]const u8) callconv(.C) *anyopaque; + extern "C" fn ffi_fclose(*anyopaque) callconv(.C) c_int; + extern "C" fn ffi_fgetc(*anyopaque) callconv(.C) c_int; + extern "C" fn ffi_fputc(c: c_int, *anyopaque) callconv(.C) c_int; + extern "C" fn ffi_feof(*anyopaque) callconv(.C) c_int; + extern "C" fn ffi_fileno(*anyopaque) callconv(.C) c_int; + extern "C" fn ffi_ungetc(c: c_int, *anyopaque) callconv(.C) c_int; + extern "C" fn ffi_ftell(*anyopaque) callconv(.C) c_long; + extern "C" fn ffi_fseek(*anyopaque, c_long, c_int) callconv(.C) c_int; + extern "C" fn ffi_fflush(*anyopaque) callconv(.C) c_int; + + extern "C" fn calloc(nmemb: usize, size: usize) callconv(.C) ?*anyopaque; + extern "C" fn perror([*:0]const u8) callconv(.C) void; + + const mac = if (Environment.isMac) struct { + var ffi_stdinp: *anyopaque = @extern(*anyopaque, .{ .name = "__stdinp" }); + var ffi_stdoutp: *anyopaque = @extern(*anyopaque, .{ .name = "__stdoutp" }); + var ffi_stderrp: *anyopaque = @extern(*anyopaque, .{ .name = "__stderrp" }); + + pub fn inject(state: *TCC.TCCState) void { + _ = TCC.tcc_add_symbol(state, "__stdinp", ffi_stdinp); + _ = TCC.tcc_add_symbol(state, "__stdoutp", ffi_stdoutp); + _ = TCC.tcc_add_symbol(state, "__stderrp", ffi_stderrp); + } + } else struct { + pub fn inject(_: *TCC.TCCState) void {} + }; + + pub fn inject(state: *TCC.TCCState) void { + _ = TCC.tcc_add_symbol(state, "vfprintf", ffi_vfprintf); + _ = TCC.tcc_add_symbol(state, "vprintf", ffi_vprintf); + _ = TCC.tcc_add_symbol(state, "fprintf", ffi_fprintf); + _ = TCC.tcc_add_symbol(state, "printf", ffi_printf); + _ = TCC.tcc_add_symbol(state, "fscanf", ffi_fscanf); + _ = TCC.tcc_add_symbol(state, "scanf", ffi_scanf); + _ = TCC.tcc_add_symbol(state, "sscanf", ffi_sscanf); + _ = TCC.tcc_add_symbol(state, "vsscanf", ffi_vsscanf); + + _ = TCC.tcc_add_symbol(state, "fopen", ffi_fopen); + _ = TCC.tcc_add_symbol(state, "fclose", ffi_fclose); + _ = TCC.tcc_add_symbol(state, "fgetc", ffi_fgetc); + _ = TCC.tcc_add_symbol(state, "fputc", ffi_fputc); + _ = TCC.tcc_add_symbol(state, "feof", ffi_feof); + _ = TCC.tcc_add_symbol(state, "fileno", ffi_fileno); + _ = TCC.tcc_add_symbol(state, "fwrite", std.c.fwrite); + _ = TCC.tcc_add_symbol(state, "ungetc", ffi_ungetc); + _ = TCC.tcc_add_symbol(state, "ftell", ffi_ftell); + _ = TCC.tcc_add_symbol(state, "fseek", ffi_fseek); + _ = TCC.tcc_add_symbol(state, "fflush", ffi_fflush); + _ = TCC.tcc_add_symbol(state, "malloc", std.c.malloc); + _ = TCC.tcc_add_symbol(state, "free", std.c.free); + _ = TCC.tcc_add_symbol(state, "fread", std.c.fread); + _ = TCC.tcc_add_symbol(state, "realloc", std.c.realloc); + _ = TCC.tcc_add_symbol(state, "calloc", calloc); + _ = TCC.tcc_add_symbol(state, "perror", perror); + + if (Environment.isPosix) { + _ = TCC.tcc_add_symbol(state, "posix_memalign", std.c.posix_memalign); + _ = TCC.tcc_add_symbol(state, "dlopen", std.c.dlopen); + _ = TCC.tcc_add_symbol(state, "dlclose", std.c.dlclose); + _ = TCC.tcc_add_symbol(state, "dlsym", std.c.dlsym); + _ = TCC.tcc_add_symbol(state, "dlerror", std.c.dlerror); + } + + mac.inject(state); + } + }; + + pub fn handleCompilationError(this: *CompileC, message: ?[*:0]const u8) callconv(.C) void { + var msg = std.mem.span(message orelse ""); + if (msg.len == 0) return; + + var offset: usize = 0; + // the message we get from TCC sometimes has garbage in it + // i think because we're doing in-memory compilation + while (offset < msg.len) : (offset += 1) { + if (msg[offset] > 0x20 and msg[offset] < 0x7f) break; + } + msg = msg[offset..]; + + this.deferred_errors.append(bun.default_allocator, bun.default_allocator.dupe(u8, msg) catch bun.outOfMemory()) catch bun.outOfMemory(); + } + + const default_tcc_options = "-std=c11 -Wl,--export-all-symbols -g"; + + pub fn compile(this: *CompileC, globalThis: *JSGlobalObject) !struct { *TCC.TCCState, []u8 } { + const state = TCC.tcc_new() orelse { + globalThis.throw("TinyCC failed to initialize", .{}); + return error.JSException; + }; + TCC.tcc_set_error_func(state, this, @ptrCast(&handleCompilationError)); + if (bun.getenvZ("BUN_TCC_OPTIONS")) |tcc_options| { + TCC.tcc_set_options(state, @ptrCast(tcc_options)); + } else { + TCC.tcc_set_options(state, default_tcc_options); + } + _ = TCC.tcc_set_output_type(state, TCC.TCC_OUTPUT_MEMORY); + errdefer TCC.tcc_delete(state); + + var pathbuf: [bun.MAX_PATH_BYTES]u8 = undefined; + + if (CompilerRT.dir()) |compiler_rt_dir| { + if (TCC.tcc_add_sysinclude_path(state, compiler_rt_dir) == -1) { + debug("TinyCC failed to add sysinclude path", .{}); + } + } + + if (Environment.isMac) { + if (bun.getenvZ("SDKROOT")) |sdkroot| { + const include_dir = bun.path.joinAbsStringBufZ(sdkroot, &pathbuf, &.{ "usr", "include" }, .auto); + if (TCC.tcc_add_sysinclude_path(state, include_dir.ptr) == -1) { + globalThis.throw("TinyCC failed to add sysinclude path", .{}); + return error.JSException; + } + + const lib_dir = bun.path.joinAbsStringBufZ(sdkroot, &pathbuf, &.{ "usr", "lib" }, .auto); + if (TCC.tcc_add_library_path(state, lib_dir.ptr) == -1) { + globalThis.throw("TinyCC failed to add library path", .{}); + return error.JSException; + } + } + + if (Environment.isAarch64) { + switch (bun.sys.directoryExistsAt(std.fs.cwd(), "/opt/homebrew/include")) { + .result => |exists| { + if (exists) { + if (TCC.tcc_add_include_path(state, "/opt/homebrew/include") == -1) { + debug("TinyCC failed to add library path", .{}); + } + } + }, + .err => {}, + } + + switch (bun.sys.directoryExistsAt(std.fs.cwd(), "/opt/homebrew/lib")) { + .result => |exists| { + if (exists) { + if (TCC.tcc_add_library_path(state, "/opt/homebrew/lib") == -1) { + debug("TinyCC failed to add library path", .{}); + } + } + }, + .err => {}, + } + } + } + + if (Environment.isPosix) { + switch (bun.sys.directoryExistsAt(std.fs.cwd(), "/usr/local/include")) { + .result => |exists| { + if (exists) { + if (TCC.tcc_add_sysinclude_path(state, "/usr/local/include") == -1) { + debug("TinyCC failed to add library path", .{}); + } + } + }, + .err => {}, + } + + switch (bun.sys.directoryExistsAt(std.fs.cwd(), "/usr/include")) { + .result => |exists| { + if (exists) { + if (TCC.tcc_add_sysinclude_path(state, "/usr/include") == -1) { + debug("TinyCC failed to add library path", .{}); + } + } + }, + .err => {}, + } + + switch (bun.sys.directoryExistsAt(std.fs.cwd(), "/usr/local/lib")) { + .result => |exists| { + if (exists) { + if (TCC.tcc_add_library_path(state, "/usr/local/lib") == -1) { + debug("TinyCC failed to add library path", .{}); + } + } + }, + .err => {}, + } + + switch (bun.sys.directoryExistsAt(std.fs.cwd(), "/usr/lib")) { + .result => |exists| { + if (exists) { + if (TCC.tcc_add_library_path(state, "/usr/lib") == -1) { + debug("TinyCC failed to add library path", .{}); + } + } + }, + .err => {}, + } + } + + if (this.deferred_errors.items.len > 0) { + return error.DeferredErrors; + } + + for (this.include_dirs.items) |include_dir| { + if (TCC.tcc_add_include_path(state, include_dir) == -1) {} + + if (this.deferred_errors.items.len > 0) { + return error.DeferredErrors; + } + } + + if (this.deferred_errors.items.len > 0) { + return error.DeferredErrors; + } + + CompilerRT.define(state); + + if (this.deferred_errors.items.len > 0) { + return error.DeferredErrors; + } + + if (TCC.tcc_add_file(state, this.source_file) != 0) { + if (this.deferred_errors.items.len > 0) { + return error.DeferredErrors; + } else { + globalThis.throw("TinyCC failed to compile", .{}); + return error.JSException; + } + } + + CompilerRT.inject(state); + stdarg.inject(state); + + if (this.deferred_errors.items.len > 0) { + return error.DeferredErrors; + } + + for (this.library_dirs.items) |library_dir| { + if (TCC.tcc_add_library_path(state, library_dir) == -1) {} + } + + if (this.deferred_errors.items.len > 0) { + return error.DeferredErrors; + } + + for (this.libraries.items) |library| { + _ = TCC.tcc_add_library(state, library); + + if (this.deferred_errors.items.len > 0) { + break; + } + } + + if (this.deferred_errors.items.len > 0) { + return error.DeferredErrors; + } + + const relocation_size = TCC.tcc_relocate(state, null); + if (this.deferred_errors.items.len > 0) { + return error.DeferredErrors; + } + + if (relocation_size < 0) { + globalThis.throw("Unexpected: tcc_relocate returned a negative value", .{}); + return error.JSException; + } + + const bytes: []u8 = try bun.default_allocator.alloc(u8, @as(usize, @intCast(relocation_size))); + // We cannot free these bytes, evidently. + + if (comptime Environment.isAarch64 and Environment.isMac) { + pthread_jit_write_protect_np(false); + } + _ = TCC.tcc_relocate(state, bytes.ptr); + if (comptime Environment.isAarch64 and Environment.isMac) { + pthread_jit_write_protect_np(true); + } + + if (this.deferred_errors.items.len > 0) { + return error.DeferredErrors; + } + + for (this.symbols.map.keys(), this.symbols.map.values()) |symbol, *function| { + const duped = bun.default_allocator.dupeZ(u8, symbol) catch bun.outOfMemory(); + defer bun.default_allocator.free(duped); + if (TCC.tcc_get_symbol(state, duped)) |function_ptr| { + function.symbol_from_dynamic_library = function_ptr; + } else { + globalThis.throw("{} is missing from {s}. Was it included in the source code?", .{ bun.fmt.quote(symbol), this.source_file }); + return error.JSException; + } + } + + if (this.deferred_errors.items.len > 0) { + return error.DeferredErrors; + } + + return .{ state, bytes }; + } + + pub fn deinit(this: *CompileC) void { + this.symbols.deinit(); + + for (this.libraries.items) |library| { + bun.default_allocator.free(library); + } + this.libraries.clearAndFree(bun.default_allocator); + + for (this.include_dirs.items) |include_dir| { + bun.default_allocator.free(include_dir); + } + this.include_dirs.clearAndFree(bun.default_allocator); + + for (this.deferred_errors.items) |deferred_error| { + bun.default_allocator.free(deferred_error); + } + this.deferred_errors.clearAndFree(bun.default_allocator); + + if (this.source_file.len > 0) + bun.default_allocator.free(this.source_file); + } + }; + const SymbolsMap = struct { + map: bun.StringArrayHashMapUnmanaged(Function) = .{}, + pub fn deinit(this: *SymbolsMap) void { + for (this.map.keys()) |key| { + bun.default_allocator.free(@constCast(key)); + } + this.map.clearAndFree(bun.default_allocator); + } + }; + + pub fn Bun__FFI__cc(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(JSC.conv) JSC.JSValue { + const arguments = callframe.arguments(1).slice(); + if (arguments.len == 0 or !arguments[0].isObject()) { + return JSC.toInvalidArguments("Expected object", .{}, globalThis); + } + + // Step 1. compile the user's code + + const object = arguments[0]; + + var compile_c = CompileC{}; + defer { + if (globalThis.hasException()) { + compile_c.deinit(); + } + } + + const symbols_object = object.get(globalThis, "symbols") orelse .zero; + if (!globalThis.hasException() and (symbols_object == .zero or !symbols_object.isObject())) { + _ = globalThis.throwInvalidArgumentTypeValue("symbols", "object", symbols_object); + } + + if (globalThis.hasException()) { + return .zero; + } + + if (generateSymbols(globalThis, &compile_c.symbols.map, symbols_object) catch JSC.JSValue.zero) |val| { + if (val != .zero and !globalThis.hasException()) + globalThis.throwValue(val); + return .zero; + } + + if (compile_c.symbols.map.count() == 0) { + globalThis.throw("Expected at least one exported symbol", .{}); + return .zero; + } + + if (object.get(globalThis, "library")) |library_value| { + if (library_value.isArray()) { + var iter = library_value.arrayIterator(globalThis); + compile_c.libraries.ensureTotalCapacityPrecise(bun.default_allocator, iter.len) catch bun.outOfMemory(); + while (iter.next()) |value| { + if (!value.isString()) { + return globalThis.throwInvalidArgumentTypeValue("library", "array of strings", value); + } + compile_c.libraries.appendAssumeCapacity(value.getZigString(globalThis).toOwnedSliceZ(bun.default_allocator) catch bun.outOfMemory()); + } + } else if (library_value.isString()) { + compile_c.libraries.append(bun.default_allocator, library_value.getZigString(globalThis).toOwnedSliceZ(bun.default_allocator) catch bun.outOfMemory()) catch bun.outOfMemory(); + } else { + return globalThis.throwInvalidArgumentTypeValue("library", "array of strings", library_value); + } + } + + if (globalThis.hasException()) { + return .zero; + } + + if (object.get(globalThis, "include")) |include_value| { + if (include_value.isArray()) { + var iter = include_value.arrayIterator(globalThis); + compile_c.include_dirs.ensureTotalCapacityPrecise(bun.default_allocator, iter.len) catch bun.outOfMemory(); + while (iter.next()) |value| { + if (!value.isString()) { + return globalThis.throwInvalidArgumentTypeValue("include", "array of strings", value); + } + compile_c.include_dirs.appendAssumeCapacity(value.getZigString(globalThis).toOwnedSliceZ(bun.default_allocator) catch bun.outOfMemory()); + } + } else if (include_value.isString()) { + compile_c.include_dirs.append(bun.default_allocator, include_value.getZigString(globalThis).toOwnedSliceZ(bun.default_allocator) catch bun.outOfMemory()) catch bun.outOfMemory(); + } else { + return globalThis.throwInvalidArgumentTypeValue("include", "array of strings", include_value); + } + } + + if (globalThis.hasException()) { + return .zero; + } + + if (object.get(globalThis, "source")) |source_value| { + if (!source_value.isString()) { + return globalThis.throwInvalidArgumentTypeValue("source", "string", source_value); + } + + const source_path = source_value.getZigString(globalThis).toOwnedSliceZ(bun.default_allocator) catch bun.outOfMemory(); + compile_c.source_file = source_path; + } + + if (globalThis.hasException()) { + return .zero; + } + + // Now we compile the code with tinycc. + var tcc_state: ?*TCC.TCCState, var bytes_to_free_on_error = compile_c.compile(globalThis) catch |err| { + switch (err) { + error.DeferredErrors => { + var combined = std.ArrayList(u8).init(bun.default_allocator); + defer combined.deinit(); + var writer = combined.writer(); + writer.print("{d} errors while compiling {s}\n", .{ compile_c.deferred_errors.items.len, compile_c.source_file }) catch bun.outOfMemory(); + + for (compile_c.deferred_errors.items) |deferred_error| { + writer.print("{s}\n", .{deferred_error}) catch bun.outOfMemory(); + } + + globalThis.throw("{s}", .{combined.items}); + return .zero; + }, + error.JSException => { + return .zero; + }, + error.OutOfMemory => { + globalThis.throwOutOfMemory(); + return .zero; + }, + } + }; + defer { + 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))); + } + } + + var obj = JSC.JSValue.createEmptyObject(globalThis, compile_c.symbols.map.count()); + for (compile_c.symbols.map.values()) |*function| { + const function_name = function.base_name.?; + 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); + return .zero; + }; + switch (function.step) { + .failed => |err| { + const res = ZigString.init(err.msg).toErrorInstance(globalThis); + globalThis.throwValue(res); + return .zero; + }, + .pending => { + globalThis.throw("Failed to compile (nothing happend!)", .{}); + return .zero; + }, + .compiled => |*compiled| { + const str = ZigString.init(bun.asByteSlice(function_name)); + const cb = JSC.NewRuntimeFunction( + globalThis, + &str, + @as(u32, @intCast(function.arg_types.items.len)), + bun.cast(JSC.JSHostFunctionPtr, compiled.ptr), + false, + true, + ); + compiled.js_function = cb; + obj.put(globalThis, &str, cb); + }, + } + } + + var lib = bun.default_allocator.create(FFI) catch bun.outOfMemory(); + lib.* = .{ + .dylib = null, + .shared_state = tcc_state, + .functions = compile_c.symbols.map, + .relocated_bytes_to_free = bytes_to_free_on_error, + }; + tcc_state = null; + bytes_to_free_on_error = ""; + compile_c.symbols = .{}; + + const js_object = lib.toJS(globalThis); + JSC.Codegen.JSFFI.symbolsValueSetCached(js_object, globalThis, obj); + return js_object; + } + pub fn closeCallback(globalThis: *JSGlobalObject, ctx: JSValue) JSValue { var function = ctx.asPtr(Function); function.deinit(globalThis, bun.default_allocator); @@ -153,12 +683,21 @@ pub const FFI = struct { this.dylib = null; } + if (this.shared_state) |state| { + this.shared_state = null; + TCC.tcc_delete(state); + } + const allocator = VirtualMachine.get().allocator; for (this.functions.values()) |*val| { val.deinit(globalThis, allocator); } this.functions.deinit(allocator); + if (this.relocated_bytes_to_free) |relocated_bytes_to_free| { + this.relocated_bytes_to_free = null; + bun.default_allocator.free(relocated_bytes_to_free); + } return .undefined; } @@ -750,24 +1289,6 @@ pub const FFI = struct { this.step = .{ .failed = .{ .msg = VirtualMachine.get().allocator.dupe(u8, msg) catch unreachable, .allocated = true } }; } - extern fn pthread_jit_write_protect_np(enable: bool) callconv(.C) void; - - const MyFunctionSStructWorkAround = struct { - JSVALUE_TO_INT64: *const fn (JSValue0: JSC.JSValue) callconv(.C) i64, - JSVALUE_TO_UINT64: *const fn (JSValue0: JSC.JSValue) callconv(.C) u64, - INT64_TO_JSVALUE: *const fn (arg0: *JSC.JSGlobalObject, arg1: i64) callconv(.C) JSC.JSValue, - UINT64_TO_JSVALUE: *const fn (arg0: *JSC.JSGlobalObject, arg1: u64) callconv(.C) JSC.JSValue, - bun_call: *const @TypeOf(JSC.C.JSObjectCallAsFunction), - }; - const headers = @import("../bindings/headers.zig"); - var workaround: MyFunctionSStructWorkAround = if (!JSC.is_bindgen) .{ - .JSVALUE_TO_INT64 = headers.JSC__JSValue__toInt64, - .JSVALUE_TO_UINT64 = headers.JSC__JSValue__toUInt64NoTruncate, - .INT64_TO_JSVALUE = headers.JSC__JSValue__fromInt64NoTruncate, - .UINT64_TO_JSVALUE = headers.JSC__JSValue__fromUInt64NoTruncate, - .bun_call = &JSC.C.JSObjectCallAsFunction, - } else undefined; - const tcc_options = "-std=c11 -nostdlib -Wl,--export-all-symbols" ++ if (Environment.isDebug) " -g" else ""; pub fn compile( @@ -812,7 +1333,7 @@ pub const FFI = struct { const compilation_result = TCC.tcc_compile_string( state, - source_code.items.ptr, + @ptrCast(source_code.items.ptr), ); // did tcc report an error? if (this.step == .failed) { @@ -871,64 +1392,6 @@ pub const FFI = struct { }; return; } - const CompilerRT = struct { - noinline fn memset( - dest: [*]u8, - c: u8, - byte_count: usize, - ) callconv(.C) void { - @memset(dest[0..byte_count], c); - } - - noinline fn memcpy( - noalias dest: [*]u8, - noalias source: [*]const u8, - byte_count: usize, - ) callconv(.C) void { - @memcpy(dest[0..byte_count], source[0..byte_count]); - } - - pub fn define(state: *TCC.TCCState) void { - if (comptime Environment.isX64) { - _ = TCC.tcc_define_symbol(state, "NEEDS_COMPILER_RT_FUNCTIONS", "1"); - // there - _ = TCC.tcc_compile_string(state, @embedFile(("libtcc1.c"))); - } - } - - pub fn inject(state: *TCC.TCCState) void { - JSC.markBinding(@src()); - _ = TCC.tcc_add_symbol(state, "memset", &memset); - _ = TCC.tcc_add_symbol(state, "memcpy", &memcpy); - - _ = TCC.tcc_add_symbol( - state, - "JSVALUE_TO_INT64_SLOW", - workaround.JSVALUE_TO_INT64, - ); - _ = TCC.tcc_add_symbol( - state, - "JSVALUE_TO_UINT64_SLOW", - workaround.JSVALUE_TO_UINT64, - ); - if (!comptime JSC.is_bindgen) { - std.mem.doNotOptimizeAway(headers.JSC__JSValue__toUInt64NoTruncate); - std.mem.doNotOptimizeAway(headers.JSC__JSValue__toInt64); - std.mem.doNotOptimizeAway(headers.JSC__JSValue__fromInt64NoTruncate); - std.mem.doNotOptimizeAway(headers.JSC__JSValue__fromUInt64NoTruncate); - } - _ = TCC.tcc_add_symbol( - state, - "INT64_TO_JSVALUE_SLOW", - workaround.INT64_TO_JSVALUE, - ); - _ = TCC.tcc_add_symbol( - state, - "UINT64_TO_JSVALUE_SLOW", - workaround.UINT64_TO_JSVALUE, - ); - } - }; pub fn compileCallback( this: *Function, @@ -971,7 +1434,7 @@ pub const FFI = struct { const compilation_result = TCC.tcc_compile_string( state, - source_code.items.ptr, + @ptrCast(source_code.items.ptr), ); // did tcc report an error? if (this.step == .failed) { @@ -1605,3 +2068,113 @@ pub const FFI = struct { } }; }; + +const CompilerRT = struct { + var compiler_rt_dir: [:0]const u8 = ""; + const compiler_rt_sources = struct { + pub const @"stdbool.h" = @embedFile("./ffi-stdbool.h"); + pub const @"stdarg.h" = @embedFile("./ffi-stdarg.h"); + pub const @"stdnoreturn.h" = @embedFile("./ffi-stdnoreturn.h"); + pub const @"stdalign.h" = @embedFile("./ffi-stdalign.h"); + pub const @"tgmath.h" = @embedFile("./ffi-tgmath.h"); + pub const @"stddef.h" = @embedFile("./ffi-stddef.h"); + pub const @"varargs.h" = "// empty"; + }; + + fn createCompilerRTDir() void { + const tmpdir = Fs.FileSystem.instance.tmpdir() catch return; + var bunCC = tmpdir.makeOpenPath("bun-cc", .{}) catch return; + defer bunCC.close(); + + inline for (comptime std.meta.declarations(compiler_rt_sources)) |decl| { + const source = @field(compiler_rt_sources, decl.name); + bunCC.writeFile(.{ + .sub_path = decl.name, + .data = source, + }) catch {}; + } + var path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; + compiler_rt_dir = bun.default_allocator.dupeZ(u8, bun.getFdPath(bunCC, &path_buf) catch return) catch bun.outOfMemory(); + } + var create_compiler_rt_dir_once = std.once(createCompilerRTDir); + + pub fn dir() ?[:0]const u8 { + create_compiler_rt_dir_once.call(); + if (compiler_rt_dir.len == 0) return null; + return compiler_rt_dir; + } + + const MyFunctionSStructWorkAround = struct { + JSVALUE_TO_INT64: *const fn (JSValue0: JSC.JSValue) callconv(.C) i64, + JSVALUE_TO_UINT64: *const fn (JSValue0: JSC.JSValue) callconv(.C) u64, + INT64_TO_JSVALUE: *const fn (arg0: *JSC.JSGlobalObject, arg1: i64) callconv(.C) JSC.JSValue, + UINT64_TO_JSVALUE: *const fn (arg0: *JSC.JSGlobalObject, arg1: u64) callconv(.C) JSC.JSValue, + bun_call: *const @TypeOf(JSC.C.JSObjectCallAsFunction), + }; + const headers = @import("../bindings/headers.zig"); + var workaround: MyFunctionSStructWorkAround = if (!JSC.is_bindgen) .{ + .JSVALUE_TO_INT64 = headers.JSC__JSValue__toInt64, + .JSVALUE_TO_UINT64 = headers.JSC__JSValue__toUInt64NoTruncate, + .INT64_TO_JSVALUE = headers.JSC__JSValue__fromInt64NoTruncate, + .UINT64_TO_JSVALUE = headers.JSC__JSValue__fromUInt64NoTruncate, + .bun_call = &JSC.C.JSObjectCallAsFunction, + } else undefined; + + noinline fn memset( + dest: [*]u8, + c: u8, + byte_count: usize, + ) callconv(.C) void { + @memset(dest[0..byte_count], c); + } + + noinline fn memcpy( + noalias dest: [*]u8, + noalias source: [*]const u8, + byte_count: usize, + ) callconv(.C) void { + @memcpy(dest[0..byte_count], source[0..byte_count]); + } + + pub fn define(state: *TCC.TCCState) void { + if (comptime Environment.isX64) { + _ = TCC.tcc_define_symbol(state, "NEEDS_COMPILER_RT_FUNCTIONS", "1"); + // there + _ = TCC.tcc_compile_string(state, @embedFile(("libtcc1.c"))); + } + } + + 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, + "JSVALUE_TO_INT64_SLOW", + workaround.JSVALUE_TO_INT64, + ); + _ = TCC.tcc_add_symbol( + state, + "JSVALUE_TO_UINT64_SLOW", + workaround.JSVALUE_TO_UINT64, + ); + if (!comptime JSC.is_bindgen) { + std.mem.doNotOptimizeAway(headers.JSC__JSValue__toUInt64NoTruncate); + std.mem.doNotOptimizeAway(headers.JSC__JSValue__toInt64); + std.mem.doNotOptimizeAway(headers.JSC__JSValue__fromInt64NoTruncate); + std.mem.doNotOptimizeAway(headers.JSC__JSValue__fromUInt64NoTruncate); + } + _ = TCC.tcc_add_symbol( + state, + "INT64_TO_JSVALUE_SLOW", + workaround.INT64_TO_JSVALUE, + ); + _ = TCC.tcc_add_symbol( + state, + "UINT64_TO_JSVALUE_SLOW", + workaround.UINT64_TO_JSVALUE, + ); + } +}; + +pub const Bun__FFI__cc = FFI.Bun__FFI__cc; diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index 851951e8db3e9..bbdc4a4fbc60d 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -5023,6 +5023,15 @@ pub const JSValue = enum(JSValueReprInt) { return this.toSliceCloneWithAllocator(globalThis, bun.default_allocator); } + /// Call `toString()` on the JSValue and clone the result. + /// On exception or out of memory, this returns null. + /// + /// Remember that `Symbol` throws an exception when you call `toString()`. + pub fn toSliceCloneZ(this: JSValue, globalThis: *JSGlobalObject) ?[:0]u8 { + var str = bun.String.tryFromJS(this, globalThis) orelse return null; + return str.toOwnedSliceZ(bun.default_allocator) catch return null; + } + /// On exception or out of memory, this returns null, to make exception checks clearer. pub fn toSliceCloneWithAllocator( this: JSValue, diff --git a/src/bun.js/bindings/c-bindings.cpp b/src/bun.js/bindings/c-bindings.cpp index 52542f03b5966..9357a1c84c6a8 100644 --- a/src/bun.js/bindings/c-bindings.cpp +++ b/src/bun.js/bindings/c-bindings.cpp @@ -1,5 +1,6 @@ // when we don't want to use @cInclude, we can just stick wrapper functions here #include "root.h" +#include #if !OS(WINDOWS) #include @@ -625,6 +626,122 @@ extern "C" void Bun__disableSOLinger(SOCKET fd) #endif +extern "C" int ffi_vprintf(const char* fmt, va_list ap) +{ + int ret = vfprintf(stderr, fmt, ap); + fflush(stderr); + return ret; +} + +extern "C" int ffi_vfprintf(FILE* stream, const char* fmt, va_list ap) +{ + int ret = vfprintf(stream, fmt, ap); + fflush(stream); + return ret; +} + +extern "C" int ffi_printf(const char* __restrict fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + int r = vprintf(fmt, ap); + va_end(ap); + fflush(stdout); + return r; +} + +extern "C" int ffi_fprintf(FILE* stream, const char* fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + int r = vfprintf(stream, fmt, ap); + va_end(ap); + fflush(stream); + return r; +} + +extern "C" int ffi_scanf(const char* fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + int r = vscanf(fmt, ap); + va_end(ap); + return r; +} + +extern "C" int ffi_fscanf(FILE* stream, const char* fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + int r = vfscanf(stream, fmt, ap); + va_end(ap); + return r; +} + +extern "C" int ffi_vsscanf(const char* str, const char* fmt, va_list ap) +{ + return vsscanf(str, fmt, ap); +} + +extern "C" int ffi_sscanf(const char* str, const char* fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + int r = vsscanf(str, fmt, ap); + va_end(ap); + return r; +} + +extern "C" FILE* ffi_fopen(const char* path, const char* mode) +{ + return fopen(path, mode); +} + +extern "C" int ffi_fclose(FILE* file) +{ + return fclose(file); +} + +extern "C" int ffi_fgetc(FILE* file) +{ + return fgetc(file); +} + +extern "C" int ffi_fputc(int c, FILE* file) +{ + return fputc(c, file); +} + +extern "C" int ffi_ungetc(int c, FILE* file) +{ + return ungetc(c, file); +} + +extern "C" int ffi_feof(FILE* file) +{ + return feof(file); +} + +extern "C" int ffi_fseek(FILE* file, long offset, int whence) +{ + return fseek(file, offset, whence); +} + +extern "C" long ffi_ftell(FILE* file) +{ + return ftell(file); +} + +extern "C" int ffi_fflush(FILE* file) +{ + return fflush(file); +} + +extern "C" int ffi_fileno(FILE* file) +{ + return fileno(file); +} + // Handle signals in bun.spawnSync. // If we receive a signal, we want to forward the signal to the child process. #if OS(LINUX) || OS(DARWIN) diff --git a/src/js/bun/ffi.ts b/src/js/bun/ffi.ts index 17dfcb0217f86..dd16088bce604 100644 --- a/src/js/bun/ffi.ts +++ b/src/js/bun/ffi.ts @@ -399,6 +399,8 @@ const native = { }, }; +const ccFn = $newZigFunction("ffi.zig", "Bun__FFI__cc", 1); + function dlopen(path, options) { if (typeof path === "string" && path?.startsWith?.("file:")) { // import.meta.url returns a file: URL @@ -446,6 +448,64 @@ function dlopen(path, options) { return result; } +function cc(options) { + if (!$isObject(options)) { + throw new Error("Expected options to be an object"); + } + + let path = options?.source; + + if (!path) { + throw new Error("Expected source to be a string to a file path"); + } + + if (typeof path === "string" && path?.startsWith?.("file:")) { + // import.meta.url returns a file: URL + // https://github.com/oven-sh/bun/issues/10304 + path = Bun.fileURLToPath(path); + } else if (typeof path === "object" && path) { + if (path instanceof URL) { + // This is mostly for import.meta.resolve() + // https://github.com/oven-sh/bun/issues/10304 + path = Bun.fileURLToPath(path as URL); + } else if (path instanceof Blob) { + // must be a Bun.file() blob + // https://discord.com/channels/876711213126520882/1230114905898614794/1230114905898614794 + path = path.name; + } + } + options.source = path; + + const result = ccFn(options); + if (result instanceof Error) throw result; + + for (let key in result.symbols) { + var symbol = result.symbols[key]; + if (options[key]?.args?.length || FFIType[options[key]?.returns as string] === FFIType.cstring) { + result.symbols[key] = FFIBuilder( + options[key].args ?? [], + options[key].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})`, + ); + } else { + // consistentcy + result.symbols[key].native = result.symbols[key]; + } + } + + // Bind it because it's a breaking change to not do so + // Previously, it didn't need to be bound + result.close = result.close.bind(result); + + return result; +} + function linkSymbols(options) { const result = nativeLinkSymbols(options); @@ -503,4 +563,5 @@ export default { toArrayBuffer, toBuffer, viewSource, + cc, }; diff --git a/src/tcc.zig b/src/tcc.zig index 10a69954cff16..a5ed153337446 100644 --- a/src/tcc.zig +++ b/src/tcc.zig @@ -1,27 +1,27 @@ pub const TCCState = opaque {}; -pub const TCCErrorFunc = ?*const fn (?*anyopaque, [*c]const u8) callconv(.C) void; +pub const TCCErrorFunc = ?*const fn (?*anyopaque, [*:0]const u8) callconv(.C) void; pub extern fn tcc_new() ?*TCCState; pub extern fn tcc_delete(s: *TCCState) void; -pub extern fn tcc_set_lib_path(s: *TCCState, path: [*c]const u8) void; +pub extern fn tcc_set_lib_path(s: *TCCState, path: [*:0]const u8) void; pub extern fn tcc_set_error_func(s: *TCCState, error_opaque: ?*anyopaque, error_func: TCCErrorFunc) void; pub extern fn tcc_get_error_func(s: *TCCState) TCCErrorFunc; pub extern fn tcc_get_error_opaque(s: *TCCState) ?*anyopaque; -pub extern fn tcc_set_options(s: *TCCState, str: [*c]const u8) void; -pub extern fn tcc_add_include_path(s: *TCCState, pathname: [*c]const u8) c_int; -pub extern fn tcc_add_sysinclude_path(s: *TCCState, pathname: [*c]const u8) c_int; -pub extern fn tcc_define_symbol(s: *TCCState, sym: [*c]const u8, value: [*c]const u8) void; -pub extern fn tcc_undefine_symbol(s: *TCCState, sym: [*c]const u8) void; -pub extern fn tcc_add_file(s: *TCCState, filename: [*c]const u8) c_int; -pub extern fn tcc_compile_string(s: *TCCState, buf: [*c]const u8) c_int; +pub extern fn tcc_set_options(s: *TCCState, str: [*:0]const u8) void; +pub extern fn tcc_add_include_path(s: *TCCState, pathname: [*:0]const u8) c_int; +pub extern fn tcc_add_sysinclude_path(s: *TCCState, pathname: [*:0]const u8) c_int; +pub extern fn tcc_define_symbol(s: *TCCState, sym: [*:0]const u8, value: [*:0]const u8) void; +pub extern fn tcc_undefine_symbol(s: *TCCState, sym: [*:0]const u8) void; +pub extern fn tcc_add_file(s: *TCCState, filename: [*:0]const u8) c_int; +pub extern fn tcc_compile_string(s: *TCCState, buf: [*:0]const u8) c_int; pub extern fn tcc_set_output_type(s: *TCCState, output_type: c_int) c_int; -pub extern fn tcc_add_library_path(s: *TCCState, pathname: [*c]const u8) c_int; -pub extern fn tcc_add_library(s: *TCCState, libraryname: [*c]const u8) c_int; +pub extern fn tcc_add_library_path(s: *TCCState, pathname: [*:0]const u8) c_int; +pub extern fn tcc_add_library(s: *TCCState, libraryname: [*:0]const u8) c_int; pub extern fn tcc_add_symbol(s: *TCCState, name: [*:0]const u8, val: *const anyopaque) c_int; -pub extern fn tcc_output_file(s: *TCCState, filename: [*c]const u8) c_int; +pub extern fn tcc_output_file(s: *TCCState, filename: [*:0]const u8) c_int; pub extern fn tcc_run(s: *TCCState, argc: c_int, argv: [*c][*c]u8) c_int; pub extern fn tcc_relocate(s1: *TCCState, ptr: ?*anyopaque) c_int; -pub extern fn tcc_get_symbol(s: *TCCState, name: [*c]const u8) ?*anyopaque; -pub extern fn tcc_list_symbols(s: *TCCState, ctx: ?*anyopaque, symbol_cb: ?*const fn (?*anyopaque, [*c]const u8, ?*const anyopaque) callconv(.C) void) void; +pub extern fn tcc_get_symbol(s: *TCCState, name: [*:0]const u8) ?*anyopaque; +pub extern fn tcc_list_symbols(s: *TCCState, ctx: ?*anyopaque, symbol_cb: ?*const fn (?*anyopaque, [*:0]const u8, ?*const anyopaque) callconv(.C) void) void; pub const TCC_OUTPUT_MEMORY = @as(c_int, 1); pub const TCC_OUTPUT_EXE = @as(c_int, 2); pub const TCC_OUTPUT_DLL = @as(c_int, 3); diff --git a/test/js/bun/ffi/__snapshots__/cc.test.ts.snap b/test/js/bun/ffi/__snapshots__/cc.test.ts.snap new file mode 100644 index 0000000000000..7f08a1de50080 --- /dev/null +++ b/test/js/bun/ffi/__snapshots__/cc.test.ts.snap @@ -0,0 +1,10 @@ +// Bun Snapshot v1, https://goo.gl/fbAQLP + +exports[`can run a .c file: cc-fixture-stderr 1`] = `"Hello, World!"`; + +exports[`can run a .c file: cc-fixture-stdout 1`] = ` +"Hello, World! +Hello, World! +Hi!, 123 == 123 +bool true = 1, bool false = 0" +`; diff --git a/test/js/bun/ffi/cc-fixture.c b/test/js/bun/ffi/cc-fixture.c new file mode 100644 index 0000000000000..5f9a428b18754 --- /dev/null +++ b/test/js/bun/ffi/cc-fixture.c @@ -0,0 +1,31 @@ +// Ensure we can include builtin headers. +#include +#include +#include +#include +#include +#if __has_include() +#include +#endif + +int main() { + +#if __has_include() + // Check fprint stdout and stderr. + fprintf(stdout, "Hello, World!\n"); + fprintf(stderr, "Hello, World!\n"); + + // Verify printf doesn't crash. + printf("Hello, World!\n"); + printf("Hi!, 123 == %d\n", 123); +#endif + + // Verify stdbool.h works. + bool g = true; + bool h = false; +#if __has_include() + printf("bool true = %d, bool false = %d\n", (int)g, (int)h); +#endif + + return 42; +} \ No newline at end of file diff --git a/test/js/bun/ffi/cc-fixture.js b/test/js/bun/ffi/cc-fixture.js new file mode 100644 index 0000000000000..013a4fb89b7f7 --- /dev/null +++ b/test/js/bun/ffi/cc-fixture.js @@ -0,0 +1,17 @@ +import { cc } from "bun:ffi"; +import fixture from "./cc-fixture.c" with { type: "file" }; +const { + symbols: { main }, +} = cc({ + source: fixture, + symbols: { + "main": { + args: [], + returns: "int", + }, + }, +}); + +if (main() !== 42) { + throw new Error("main() !== 42"); +} diff --git a/test/js/bun/ffi/cc.test.ts b/test/js/bun/ffi/cc.test.ts new file mode 100644 index 0000000000000..c7a1236f51c87 --- /dev/null +++ b/test/js/bun/ffi/cc.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from "bun:test"; +import path from "path"; + +import { bunExe, bunEnv, isCI } from "harness"; + +// TODO: we need to install build-essential and apple SDK in CI. +// it can't find includes. It can on machiens with that enabled. +it.todoIf(isCI)("can run a .c file", () => { + const result = Bun.spawnSync({ + cmd: [bunExe(), path.join(__dirname, "cc-fixture.js")], + cwd: __dirname, + env: bunEnv, + stdio: ["inherit", "inherit", "inherit"], + }); + + expect(result.exitCode).toBe(42); +});