diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e58c8c4..218f719 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -188,7 +188,7 @@ jobs: - uses: actions/checkout@v6 - name: build run: | - cmake -B build -DQJS_BUILD_WERROR=ON -DQJS_BUILD_EXAMPLES=ON -G "Visual Studio 17 2022" -A ${{matrix.config.arch}} + cmake -B build -DQJS_BUILD_WERROR=ON -DQJS_BUILD_EXAMPLES=ON -A ${{matrix.config.arch}} cmake --build build --config ${{matrix.config.buildType}} - name: stats run: | @@ -264,7 +264,7 @@ jobs: - name: build run: | git submodule update --init --checkout --depth 1 - cmake -B build -DQJS_BUILD_WERROR=ON -DQJS_BUILD_EXAMPLES=ON -G "Visual Studio 17 2022" -T ClangCL + cmake -B build -DQJS_BUILD_WERROR=ON -DQJS_BUILD_EXAMPLES=ON -T ClangCL cmake --build build --config ${{matrix.buildType}} - name: stats run: | @@ -721,9 +721,11 @@ jobs: args: >- "-Db_sanitize=address,undefined" "-Db_lundef=false" + "-Db_vscrt=md" extra_envs: CC: clang-cl CXX: clang-cl + ASAN_OPTIONS: report_globals=0:halt_on_error=1:abort_on_error=1:print_summary=1 platform: - ubuntu-latest - windows-latest @@ -818,6 +820,18 @@ jobs: - name: Checkout uses: actions/checkout@v6 + - name: Set up Clang sanitizer runtime + if: ${{ matrix.platform == 'windows-latest' && matrix.mode.name == 'clang-cl+sanitize' }} + run: | + $clangResourceDir = (& clang-cl --print-resource-dir).Trim() + $clangRuntimeDir = Join-Path $clangResourceDir "lib\windows" + $asanRuntime = Join-Path $clangRuntimeDir "clang_rt.asan_dynamic-x86_64.lib" + $asanThunk = Join-Path $clangRuntimeDir "clang_rt.asan_dynamic_runtime_thunk-x86_64.lib" + if (!(Test-Path $asanRuntime) -or !(Test-Path $asanThunk)) { + throw "Clang ASan runtime libraries not found in: $clangRuntimeDir" + } + "LIB=$clangRuntimeDir;$env:LIB" >> $env:GITHUB_ENV + $clangRuntimeDir >> $env:GITHUB_PATH - name: Activate MSVC and Configure if: ${{ matrix.platform == 'windows-latest' }} env: ${{ matrix.mode.extra_envs }} diff --git a/.gitignore b/.gitignore index ff10fcd..9d4812d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.DS_Store *.a *.orig *.so diff --git a/README.md b/README.md index e4b0e34..a57aa42 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,12 @@ This project is a _fork_ of the [original QuickJS project] by Fabrice Bellard an Head over to the [project website] for instructions on how to get started and more documentation. +## Zephyr + +QuickJS can use Zephyr's libc and native synchronization primitives, including 64-bit Atomics on +targets without lock-free 64-bit instructions. The [Zephyr example](examples/zephyr/README.md) +builds and runs the engine directly on the `mps2/an385` QEMU board. + ## Authors [@bnoordhuis], [@saghul], and many more [contributors]. diff --git a/cutils.h b/cutils.h index f8caaa6..9f5bf05 100644 --- a/cutils.h +++ b/cutils.h @@ -29,7 +29,9 @@ #include #include #include -#if !defined(_MSC_VER) && !defined(RQUICKJS_WASM_FREESTANDING) +#if defined(__ZEPHYR__) +#include +#elif !defined(_MSC_VER) && !defined(RQUICKJS_WASM_FREESTANDING) #include #endif #if defined(__APPLE__) @@ -66,7 +68,10 @@ uint64_t rquickjs_browser_hrtime_ns(void); #include #include // _beginthread #endif -#if !defined(_WIN32) && !defined(EMSCRIPTEN) && !defined(__wasi__) && !defined(__DJGPP) && !defined(RQUICKJS_WASM_FREESTANDING) +#if defined(__ZEPHYR__) +#include +#include +#elif !defined(_WIN32) && !defined(EMSCRIPTEN) && !defined(__wasi__) && !defined(__DJGPP) && !defined(RQUICKJS_WASM_FREESTANDING) #include #include #endif @@ -87,10 +92,16 @@ uint64_t rquickjs_browser_hrtime_ns(void); # define __attribute__(x) # define __attribute(x) #else -# define likely(x) __builtin_expect(!!(x), 1) -# define unlikely(x) __builtin_expect(!!(x), 0) +# ifndef likely +# define likely(x) __builtin_expect(!!(x), 1) +# endif +# ifndef unlikely +# define unlikely(x) __builtin_expect(!!(x), 0) +# endif # define no_inline __attribute__((noinline)) -# define __maybe_unused __attribute__((unused)) +# ifndef __maybe_unused +# define __maybe_unused __attribute__((unused)) +# endif #endif #ifndef offsetof @@ -628,7 +639,13 @@ static inline int js_exepath(char* buffer, size_t* size); #define JS_HAVE_THREADS 1 -#if defined(_WIN32) +#if defined(__ZEPHYR__) +#define JS_ONCE_INIT ATOMIC_INIT(0) +typedef atomic_t js_once_t; +typedef struct k_mutex js_mutex_t; +typedef struct k_condvar js_cond_t; +typedef pthread_t js_thread_t; +#elif defined(_WIN32) #define JS_ONCE_INIT INIT_ONCE_STATIC_INIT typedef INIT_ONCE js_once_t; typedef CRITICAL_SECTION js_mutex_t; @@ -1726,7 +1743,100 @@ static inline int js_exepath(char* buffer, size_t* size_ptr) { /*--- Cross-platform threading APIs. ----*/ #if JS_HAVE_THREADS -#if defined(_WIN32) +#if defined(__ZEPHYR__) + +static inline void js_once(js_once_t *guard, void (*callback)(void)) { + if (atomic_cas(guard, 0, 1)) { + callback(); + atomic_set(guard, 2); + } else { + while (atomic_get(guard) != 2) + k_yield(); + } +} + +static inline void js_mutex_init(js_mutex_t *mutex) { + if (k_mutex_init(mutex)) + abort(); +} + +static inline void js_mutex_destroy(js_mutex_t *mutex) { + (void)mutex; +} + +static inline void js_mutex_lock(js_mutex_t *mutex) { + if (k_mutex_lock(mutex, K_FOREVER)) + abort(); +} + +static inline void js_mutex_unlock(js_mutex_t *mutex) { + if (k_mutex_unlock(mutex)) + abort(); +} + +static inline void js_cond_init(js_cond_t *cond) { + if (k_condvar_init(cond)) + abort(); +} + +static inline void js_cond_destroy(js_cond_t *cond) { + (void)cond; +} + +static inline void js_cond_signal(js_cond_t *cond) { + if (k_condvar_signal(cond)) + abort(); +} + +static inline void js_cond_broadcast(js_cond_t *cond) { + if (k_condvar_broadcast(cond) < 0) + abort(); +} + +static inline void js_cond_wait(js_cond_t *cond, js_mutex_t *mutex) { + if (k_condvar_wait(cond, mutex, K_FOREVER)) + abort(); +} + +static inline int js_cond_timedwait(js_cond_t *cond, js_mutex_t *mutex, + uint64_t timeout) { + int ret = k_condvar_wait(cond, mutex, K_NSEC(timeout)); + if (ret == 0) + return 0; + if (ret == -EAGAIN) { + if (k_mutex_lock(mutex, K_FOREVER)) + abort(); + return -1; + } + abort(); +} + +static inline int js_thread_create(js_thread_t *thrd, void (*start)(void *), void *arg, + int flags) { + union { + void (*x)(void *); + void *(*f)(void *); + } u = {start}; + pthread_attr_t attr; + int ret = -1; + + if (flags & ~JS_THREAD_CREATE_DETACHED || pthread_attr_init(&attr)) + return -1; + if ((flags & JS_THREAD_CREATE_DETACHED) && + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED)) + goto fail; + if (!pthread_create(thrd, &attr, u.f, arg)) + ret = 0; +fail: + pthread_attr_destroy(&attr); + return ret; +} + +static inline int js_thread_join(js_thread_t thrd) { + return pthread_join(thrd, NULL) ? -1 : 0; +} + +#elif defined(_WIN32) typedef void (*js__once_cb)(void); typedef struct { diff --git a/examples/zephyr/CMakeLists.txt b/examples/zephyr/CMakeLists.txt new file mode 100644 index 0000000..dbaf0c0 --- /dev/null +++ b/examples/zephyr/CMakeLists.txt @@ -0,0 +1,16 @@ +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(quickjs_zephyr) + +get_filename_component(QUICKJS_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../.." ABSOLUTE) + +target_sources(app PRIVATE + src/main.c + ${QUICKJS_ROOT}/quickjs.c + ${QUICKJS_ROOT}/dtoa.c + ${QUICKJS_ROOT}/libregexp.c + ${QUICKJS_ROOT}/libunicode.c +) +target_include_directories(app PRIVATE ${QUICKJS_ROOT}) +target_compile_definitions(app PRIVATE _GNU_SOURCE) diff --git a/examples/zephyr/README.md b/examples/zephyr/README.md new file mode 100644 index 0000000..f2b16e1 --- /dev/null +++ b/examples/zephyr/README.md @@ -0,0 +1,17 @@ +# Zephyr example + +This sample builds the QuickJS engine directly into a Zephyr application and validates the C API, +promises and pending jobs, SharedArrayBuffer, timed waits, and 8/16/32/64-bit Atomics. + +Zephyr 4.4.2 and SDK 1.0.1 were used for the initial port. The `mps2/an385` QEMU board has enough +RAM and flash for the full engine. + +```sh +source /Users/yorkie/zephyrproject/env.sh +cd /Users/yorkie/zephyrproject +west build -b mps2/an385 /Users/yorkie/workspace/jsar-project/quickjs/examples/zephyr \ + -d build/quickjs-zephyr +west build -d build/quickjs-zephyr -t run +``` + +Exit QEMU with Ctrl+A, then X. diff --git a/examples/zephyr/prj.conf b/examples/zephyr/prj.conf new file mode 100644 index 0000000..e78d132 --- /dev/null +++ b/examples/zephyr/prj.conf @@ -0,0 +1,11 @@ +CONFIG_MAIN_STACK_SIZE=65536 +CONFIG_HEAP_MEM_POOL_SIZE=2097152 + +CONFIG_POSIX_API=y +CONFIG_POSIX_THREAD_THREADS_MAX=4 +CONFIG_MAX_PTHREAD_MUTEX_COUNT=8 +CONFIG_MAX_PTHREAD_COND_COUNT=8 +CONFIG_POSIX_THREAD_ATTR_STACKSIZE=y + +CONFIG_LOG=y +CONFIG_LOG_BACKEND_RTT=n diff --git a/examples/zephyr/src/main.c b/examples/zephyr/src/main.c new file mode 100644 index 0000000..f7563e6 --- /dev/null +++ b/examples/zephyr/src/main.c @@ -0,0 +1,108 @@ +#include +#include + +#include + +#include "quickjs.h" + +static int report_exception(JSContext *ctx, const char *stage) +{ + JSValue exception = JS_GetException(ctx); + const char *message = JS_ToCString(ctx, exception); + + printk("QuickJS %s failed: %s\n", stage, message ? message : "unknown exception"); + JS_FreeCString(ctx, message); + JS_FreeValue(ctx, exception); + return -1; +} + +int main(void) +{ + JSRuntime *runtime = JS_NewRuntime(); + JSContext *context; + JSContext *job_context; + JSValue value; + int32_t result; + + if (runtime == NULL) { + printk("QuickJS runtime allocation failed\n"); + return 1; + } + JS_SetCanBlock(runtime, true); + + context = JS_NewContext(runtime); + if (context == NULL) { + printk("QuickJS context allocation failed\n"); + JS_FreeRuntime(runtime); + return 1; + } + + value = JS_Eval(context, + "globalThis.answer = 0;" + "Promise.resolve(40).then(v => { globalThis.answer = v + 2; });", + sizeof("globalThis.answer = 0;" + "Promise.resolve(40).then(v => { globalThis.answer = v + 2; });") - 1, + "", JS_EVAL_TYPE_GLOBAL); + if (JS_IsException(value)) { + JS_FreeValue(context, value); + report_exception(context, "promise evaluation"); + goto fail; + } + JS_FreeValue(context, value); + + while (JS_IsJobPending(runtime)) { + if (JS_ExecutePendingJob(runtime, &job_context) < 0) { + report_exception(job_context, "pending job"); + goto fail; + } + } + + static const char atomics_script[] = + "(() => {" + " const sab = new SharedArrayBuffer(32);" + " const u8 = new Uint8Array(sab);" + " const i16 = new Int16Array(sab);" + " const i32 = new Int32Array(sab);" + " const i64 = new BigInt64Array(sab);" + " if (Atomics.add(i32, 0, 40) !== 0) return -1;" + " if (Atomics.compareExchange(i32, 0, 40, 42) !== 40) return -2;" + " if (Atomics.wait(i32, 1, 0, 1) !== 'timed-out') return -3;" + " if (Atomics.exchange(u8, 16, 5) !== 0) return -4;" + " if (Atomics.or(u8, 16, 2) !== 5) return -5;" + " if (Atomics.store(i16, 9, 9) !== 9) return -6;" + " if (Atomics.xor(i16, 9, 5) !== 9) return -7;" + " if (Atomics.store(i64, 1, 7n) !== 7n) return -8;" + " if (Atomics.add(i64, 1, 2n) !== 7n) return -9;" + " if (Atomics.load(i64, 1) !== 9n) return -10;" + " return globalThis.answer;" + "})()"; + + value = JS_Eval(context, atomics_script, sizeof(atomics_script) - 1, + "", JS_EVAL_TYPE_GLOBAL); + if (JS_IsException(value)) { + JS_FreeValue(context, value); + report_exception(context, "Atomics evaluation"); + goto fail; + } + if (JS_ToInt32(context, &result, value) < 0) { + JS_FreeValue(context, value); + report_exception(context, "result conversion"); + goto fail; + } + JS_FreeValue(context, value); + + printk("QuickJS Zephyr returned %ld\n", (long)result); + if (result != 42) { + goto fail; + } + + printk("QuickJS Zephyr: PASS\n"); + JS_FreeContext(context); + JS_FreeRuntime(runtime); + return 0; + +fail: + JS_FreeContext(context); + JS_FreeRuntime(runtime); + return 1; +} diff --git a/meson.build b/meson.build index e3bc0bd..5b3b1e1 100644 --- a/meson.build +++ b/meson.build @@ -91,6 +91,16 @@ if host_system == 'windows' stack_size = 8 * 1024 * 1024 if cc.get_argument_syntax() == 'msvc' add_project_link_arguments(f'/STACK:@stack_size@', language: 'c') + if cc.get_id() == 'clang-cl' and get_option('b_sanitize').contains('address') + clang_rt_arch = host_machine.cpu_family() == 'x86' ? 'i386' : host_machine.cpu_family() + add_project_link_arguments( + f'clang_rt.asan_dynamic-@clang_rt_arch@.lib', + '/include:__asan_seh_interceptor', + f'/wholearchive:clang_rt.asan_dynamic_runtime_thunk-@clang_rt_arch@.lib', + '/incremental:no', + language: 'c', + ) + endif else add_project_link_arguments(f'-Wl,--stack,@stack_size@', language: 'c') endif diff --git a/quickjs-c-atomics.h b/quickjs-c-atomics.h index 8fc6b72..f3e6d8f 100644 --- a/quickjs-c-atomics.h +++ b/quickjs-c-atomics.h @@ -22,14 +22,71 @@ * THE SOFTWARE. */ -#if (defined(__GNUC__) || defined(__GNUG__)) && !defined(__clang__) +#if defined(__ZEPHYR__) +#include + +/* Cortex-M does not provide lock-free 64-bit atomics and the Zephyr SDK does + * not ship libatomic. Serialize all SharedArrayBuffer operations with one + * kernel spin lock so the complete Atomics API remains available. */ +static struct k_spinlock js_atomic_lock; + +#define _Atomic +#define JS_ATOMIC_UPDATE(obj, arg, expression) \ + __extension__ ({ \ + __auto_type js_obj = (obj); \ + __typeof__(*js_obj) js_arg = (arg); \ + k_spinlock_key_t js_key = k_spin_lock(&js_atomic_lock); \ + __typeof__(*js_obj) js_old = *js_obj; \ + *js_obj = (expression); \ + k_spin_unlock(&js_atomic_lock, js_key); \ + js_old; \ + }) +#define atomic_fetch_add(obj, arg) JS_ATOMIC_UPDATE(obj, arg, js_old + js_arg) +#define atomic_fetch_or(obj, arg) JS_ATOMIC_UPDATE(obj, arg, js_old | js_arg) +#define atomic_fetch_xor(obj, arg) JS_ATOMIC_UPDATE(obj, arg, js_old ^ js_arg) +#define atomic_fetch_and(obj, arg) JS_ATOMIC_UPDATE(obj, arg, js_old & js_arg) +#define atomic_fetch_sub(obj, arg) JS_ATOMIC_UPDATE(obj, arg, js_old - js_arg) +#define atomic_exchange(obj, arg) JS_ATOMIC_UPDATE(obj, arg, js_arg) +#define atomic_load(obj) \ + __extension__ ({ \ + __auto_type js_obj = (obj); \ + k_spinlock_key_t js_key = k_spin_lock(&js_atomic_lock); \ + __typeof__(*js_obj) js_value = *js_obj; \ + k_spin_unlock(&js_atomic_lock, js_key); \ + js_value; \ + }) +#define atomic_store(obj, desired) \ + do { \ + __auto_type js_obj = (obj); \ + k_spinlock_key_t js_key = k_spin_lock(&js_atomic_lock); \ + *js_obj = (desired); \ + k_spin_unlock(&js_atomic_lock, js_key); \ + } while (0) +#define atomic_compare_exchange_strong(obj, expected, desired) \ + __extension__ ({ \ + __auto_type js_obj = (obj); \ + __auto_type js_expected = (expected); \ + bool js_success; \ + k_spinlock_key_t js_key = k_spin_lock(&js_atomic_lock); \ + if (*js_obj == *js_expected) { \ + *js_obj = (desired); \ + js_success = true; \ + } else { \ + *js_expected = *js_obj; \ + js_success = false; \ + } \ + k_spin_unlock(&js_atomic_lock, js_key); \ + js_success; \ + }) + +#elif (defined(__GNUC__) || defined(__GNUG__)) && !defined(__clang__) // Use GCC builtins for version < 4.9 # if((__GNUC__ << 16) + __GNUC_MINOR__ < ((4) << 16) + 9) # define GCC_BUILTIN_ATOMICS # endif #endif -#ifdef GCC_BUILTIN_ATOMICS +#if !defined(__ZEPHYR__) && defined(GCC_BUILTIN_ATOMICS) #define atomic_fetch_add(obj, arg) \ __atomic_fetch_add(obj, arg, __ATOMIC_SEQ_CST) #define atomic_compare_exchange_strong(obj, expected, desired) \ @@ -49,6 +106,6 @@ #define atomic_fetch_sub(obj, arg) \ __atomic_fetch_sub(obj, arg, __ATOMIC_SEQ_CST) #define _Atomic -#else +#elif !defined(__ZEPHYR__) #include #endif diff --git a/quickjs.c b/quickjs.c index 7576620..cc0c731 100644 --- a/quickjs.c +++ b/quickjs.c @@ -31,7 +31,9 @@ #include #include #include -#if !defined(_MSC_VER) && !defined(RQUICKJS_WASM_FREESTANDING) +#if defined(__ZEPHYR__) +#include +#elif !defined(_MSC_VER) && !defined(RQUICKJS_WASM_FREESTANDING) #include #if defined(_WIN32) #include @@ -90,6 +92,12 @@ #define check_dump_flag(rt, flag) ((rt->dump_flags & (flag +0)) == (flag +0)) +#ifdef STRINGIFY +#undef STRINGIFY +#endif +#ifdef STRINGIFY_ +#undef STRINGIFY_ +#endif #define STRINGIFY_(x) #x #define STRINGIFY(x) STRINGIFY_(x) @@ -7635,7 +7643,8 @@ static int find_line_num(JSContext *ctx, JSFunctionBytecode *b, uint32_t pc_value, int *col) { const uint8_t *p_end, *p; - int new_line_num, new_col_num, line_num, col_num, pc, v, ret; + int new_line_num, new_col_num, line_num, col_num, pc, ret; + int32_t v; unsigned int op; *col = 1; @@ -44685,7 +44694,8 @@ static JSValue js_parseInt(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { const char *str, *p; - int radix, flags; + int32_t radix; + int flags; JSValue ret; str = JS_ToCString(ctx, argv[0]); @@ -53323,7 +53333,7 @@ static __exception int remainingElementsCount_add(JSContext *ctx, int addend) { JSValue val; - int remainingElementsCount; + int32_t remainingElementsCount; val = JS_GetPropertyUint32(ctx, resolve_element_env, 0); if (JS_IsException(val)) @@ -53354,7 +53364,8 @@ static JSValue js_promise_all_resolve_element(JSContext *ctx, JSValueConst resolve = func_data[3]; JSValueConst resolve_element_env = func_data[4]; JSValue ret, obj; - int is_zero, index; + int is_zero; + int32_t index; if (JS_ToInt32(ctx, &index, func_data[1])) return JS_EXCEPTION; @@ -59738,9 +59749,9 @@ static JSValue js_atomics_wait(JSContext *ctx, 'ptr' value */ js_mutex_lock(&js_atomics_mutex); if (size_log2 == 3) { - res = *(int64_t *)ptr != v; + res = atomic_load((_Atomic int64_t *)ptr) != v; } else { - res = *(int32_t *)ptr != v; + res = atomic_load((_Atomic int32_t *)ptr) != v; } if (res) { js_mutex_unlock(&js_atomics_mutex); @@ -59776,7 +59787,7 @@ static JSValue js_atomics_notify(JSContext *ctx, { struct list_head *el, *el1, waiter_list; int size_log2; - int32_t count, n; + int count, n; void *ptr; uint64_t idx; JSObject *p;