From 9604f3cab2b9204fd4bff6ebfe8a6224a0b24764 Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Thu, 30 Jul 2026 22:09:46 +0500 Subject: [PATCH 1/7] refactor(jsi): start the shared engine layer with the neutral GSD core First step of the Apple-side extraction (branch 2a). Introduces NativeScript/jsi as the platform-neutral home for the engine binding layer that Android will also compile, and moves in the pieces that are already neutral. Moved verbatim: ffi/objc/shared/NativeApiBackendConfig.h -> jsi/shared/ ffi/objc/shared/Tasks.{h,cpp} -> jsi/shared/ SignatureDispatchCore.h could not simply move: it is only half neutral. The mechanism (FNV-1a hashing, SignatureCallKind, the prepared-invoker and dispatch-entry types, the sorted-table lookup, the NS_DISABLE_GSD switch) is common, but the rest walks the Objective-C metadata through metagen::MDMetadataReader and metagen::MDTypeKind. Android's metadata is a different binary format, so that half stays under ffi/objc. The neutral half is now jsi/shared/SignatureHashing.h, which SignatureDispatchCore.h includes. Every consumer includes these through their full "ffi/objc/shared/..." path, so forwarding headers are left behind and no Apple include or source file changes. The only other Apple change is the CMake path for Tasks.cpp. Adds scripts/check_jsi_layer_neutral.sh, which fails if Objective-C or a .mm file appears under NativeScript/jsi. This is not paranoia: `#import ` compiles fine under -x c++ on macOS, so a green Apple build does not prove the layer is Android-compilable, and the regression would only surface much later on the Android side. Verified: iOS builds and reports 713 specs / 0 failures, unchanged from the parent branch; the shared header also compiles standalone with `clang++ -x c++ -std=c++20`. No Android file is touched by this branch. (cherry picked from commit 9d62ca31c1ea543eb647325c9b8b6655dc44a749) --- NativeScript/CMakeLists.txt | 2 +- .../ffi/objc/shared/NativeApiBackendConfig.h | 38 +----- .../ffi/objc/shared/SignatureDispatchCore.h | 90 +------------- NativeScript/ffi/objc/shared/Tasks.h | 24 +--- .../jsi/shared/NativeApiBackendConfig.h | 34 ++++++ NativeScript/jsi/shared/SignatureHashing.h | 110 ++++++++++++++++++ .../{ffi/objc => jsi}/shared/Tasks.cpp | 0 NativeScript/jsi/shared/Tasks.h | 20 ++++ scripts/check_jsi_layer_neutral.sh | 39 +++++++ 9 files changed, 222 insertions(+), 135 deletions(-) create mode 100644 NativeScript/jsi/shared/NativeApiBackendConfig.h create mode 100644 NativeScript/jsi/shared/SignatureHashing.h rename NativeScript/{ffi/objc => jsi}/shared/Tasks.cpp (100%) create mode 100644 NativeScript/jsi/shared/Tasks.h create mode 100755 scripts/check_jsi_layer_neutral.sh diff --git a/NativeScript/CMakeLists.txt b/NativeScript/CMakeLists.txt index bb48fb6fc..a34d840ec 100644 --- a/NativeScript/CMakeLists.txt +++ b/NativeScript/CMakeLists.txt @@ -237,7 +237,7 @@ include_directories( ) set(FFI_SHARED_SOURCE_FILES - ffi/objc/shared/Tasks.cpp + jsi/shared/Tasks.cpp ) set(FFI_NAPI_SOURCE_FILES diff --git a/NativeScript/ffi/objc/shared/NativeApiBackendConfig.h b/NativeScript/ffi/objc/shared/NativeApiBackendConfig.h index 859526f05..bc229fd2f 100644 --- a/NativeScript/ffi/objc/shared/NativeApiBackendConfig.h +++ b/NativeScript/ffi/objc/shared/NativeApiBackendConfig.h @@ -1,34 +1,8 @@ -#ifndef NATIVESCRIPT_FFI_SHARED_NATIVE_API_BACKEND_CONFIG_H -#define NATIVESCRIPT_FFI_SHARED_NATIVE_API_BACKEND_CONFIG_H +#ifndef NATIVESCRIPT_FFI_SHARED_NATIVE_API_BACKEND_CONFIG_FORWARD_H +#define NATIVESCRIPT_FFI_SHARED_NATIVE_API_BACKEND_CONFIG_FORWARD_H -#include -#include +// Moved to the shared JSI layer; this forwarding header keeps existing +// Apple include paths working unchanged. +#include "jsi/shared/NativeApiBackendConfig.h" -namespace nativescript { - -class NativeApiBackendScheduler { - public: - virtual ~NativeApiBackendScheduler() = default; - virtual void invokeOnJS(std::function task) = 0; - virtual void invokeOnUI(std::function task) = 0; -}; - -struct NativeApiBackendConfig { - const char* metadataPath = nullptr; - const void* metadataPtr = nullptr; - const char* globalName = "__nativeScriptNativeApi"; - std::shared_ptr scheduler = nullptr; - std::function)> nativeInvocationInvoker = nullptr; - std::function)> nativeCallbackInvoker = nullptr; - std::function)> runtimeCallbackInvoker = nullptr; - std::function)> jsThreadCallbackInvoker = nullptr; - std::function)> jsThreadAsyncCallbackInvoker = nullptr; - std::function callbackInvocationAllowed = nullptr; - bool invokeCallbacksOnNativeCallerThread = false; - bool installGlobalSymbols = false; - bool indexRuntimePointers = true; -}; - -} // namespace nativescript - -#endif // NATIVESCRIPT_FFI_SHARED_NATIVE_API_BACKEND_CONFIG_H +#endif // NATIVESCRIPT_FFI_SHARED_NATIVE_API_BACKEND_CONFIG_FORWARD_H diff --git a/NativeScript/ffi/objc/shared/SignatureDispatchCore.h b/NativeScript/ffi/objc/shared/SignatureDispatchCore.h index 229347a74..c53fd26dc 100644 --- a/NativeScript/ffi/objc/shared/SignatureDispatchCore.h +++ b/NativeScript/ffi/objc/shared/SignatureDispatchCore.h @@ -10,95 +10,17 @@ #include "Metadata.h" #include "MetadataReader.h" +// The platform-neutral half of GSD (FNV-1a hashing, call kinds, dispatch-entry +// types and table lookup) lives in the shared JSI layer so the Android runtime +// can use the same mechanism. Everything below is Objective-C metadata specific +// and stays here. +#include "jsi/shared/SignatureHashing.h" + namespace nativescript { -enum class SignatureCallKind : uint8_t { - ObjCMethod = 1, - CFunction = 2, - BlockInvoke = 3, -}; - -using ObjCPreparedInvoker = void (*)(void* fnptr, void** avalues, - void* rvalue); -using CFunctionPreparedInvoker = void (*)(void* fnptr, void** avalues, - void* rvalue); -using BlockPreparedInvoker = void (*)(void* fnptr, void** avalues, - void* rvalue); - -struct ObjCDispatchEntry { - uint64_t dispatchId; - ObjCPreparedInvoker invoker; -}; - -struct CFunctionDispatchEntry { - uint64_t dispatchId; - CFunctionPreparedInvoker invoker; -}; - -struct BlockDispatchEntry { - uint64_t dispatchId; - BlockPreparedInvoker invoker; -}; - -inline constexpr uint64_t kSignatureHashOffsetBasis = 14695981039346656037ull; -inline constexpr uint64_t kSignatureHashPrime = 1099511628211ull; inline constexpr metagen::MDSectionOffset kNullMetadataSectionOffset = static_cast(0xFFFFFFFFu >> 1); -inline uint64_t hashBytesFnv1a(const void* data, size_t size, - uint64_t seed = kSignatureHashOffsetBasis) { - const auto* bytes = static_cast(data); - uint64_t hash = seed; - for (size_t i = 0; i < size; i++) { - hash ^= static_cast(bytes[i]); - hash *= kSignatureHashPrime; - } - return hash; -} - -inline uint64_t composeSignatureDispatchId(uint64_t signatureHash, - SignatureCallKind kind, - uint8_t flags) { - const uint8_t kindByte = static_cast(kind); - uint64_t hash = hashBytesFnv1a(&kindByte, sizeof(kindByte)); - hash = hashBytesFnv1a(&flags, sizeof(flags), hash); - return hashBytesFnv1a(&signatureHash, sizeof(signatureHash), hash); -} - -template -inline Invoker lookupDispatchInvoker(const Entry (&entries)[N], - uint64_t dispatchId) { - if (dispatchId == 0 || N <= 1) { - return nullptr; - } - - size_t low = 1; - size_t high = N; - while (low < high) { - const size_t mid = low + ((high - low) >> 1); - const uint64_t midId = entries[mid].dispatchId; - if (midId < dispatchId) { - low = mid + 1; - } else { - high = mid; - } - } - - if (low < N && entries[low].dispatchId == dispatchId) { - return entries[low].invoker; - } - return nullptr; -} - -inline bool isGeneratedDispatchEnabled() { - static const bool enabled = []() { - const char* disableFlag = std::getenv("NS_DISABLE_GSD"); - return disableFlag == nullptr || disableFlag[0] == '\0' || - (disableFlag[0] == '0' && disableFlag[1] == '\0'); - }(); - return enabled; -} - namespace signature_dispatch_detail { inline metagen::MDTypeKind canonicalizeSignatureTypeKind( diff --git a/NativeScript/ffi/objc/shared/Tasks.h b/NativeScript/ffi/objc/shared/Tasks.h index cd3b4a68c..8b9c8a2d3 100644 --- a/NativeScript/ffi/objc/shared/Tasks.h +++ b/NativeScript/ffi/objc/shared/Tasks.h @@ -1,20 +1,8 @@ -#ifndef Tasks_h -#define Tasks_h +#ifndef NATIVESCRIPT_FFI_SHARED_TASKS_FORWARD_H +#define NATIVESCRIPT_FFI_SHARED_TASKS_FORWARD_H -#include -#include +// Moved to the shared JSI layer; this forwarding header keeps existing +// Apple include paths working unchanged. +#include "jsi/shared/Tasks.h" -namespace nativescript { - -class Tasks { -public: - static void Register(std::function task); - static void Drain(); - static void ClearTasks(); -private: - static std::vector> tasks_; -}; - -} - -#endif /* Tasks_h */ +#endif // NATIVESCRIPT_FFI_SHARED_TASKS_FORWARD_H diff --git a/NativeScript/jsi/shared/NativeApiBackendConfig.h b/NativeScript/jsi/shared/NativeApiBackendConfig.h new file mode 100644 index 000000000..859526f05 --- /dev/null +++ b/NativeScript/jsi/shared/NativeApiBackendConfig.h @@ -0,0 +1,34 @@ +#ifndef NATIVESCRIPT_FFI_SHARED_NATIVE_API_BACKEND_CONFIG_H +#define NATIVESCRIPT_FFI_SHARED_NATIVE_API_BACKEND_CONFIG_H + +#include +#include + +namespace nativescript { + +class NativeApiBackendScheduler { + public: + virtual ~NativeApiBackendScheduler() = default; + virtual void invokeOnJS(std::function task) = 0; + virtual void invokeOnUI(std::function task) = 0; +}; + +struct NativeApiBackendConfig { + const char* metadataPath = nullptr; + const void* metadataPtr = nullptr; + const char* globalName = "__nativeScriptNativeApi"; + std::shared_ptr scheduler = nullptr; + std::function)> nativeInvocationInvoker = nullptr; + std::function)> nativeCallbackInvoker = nullptr; + std::function)> runtimeCallbackInvoker = nullptr; + std::function)> jsThreadCallbackInvoker = nullptr; + std::function)> jsThreadAsyncCallbackInvoker = nullptr; + std::function callbackInvocationAllowed = nullptr; + bool invokeCallbacksOnNativeCallerThread = false; + bool installGlobalSymbols = false; + bool indexRuntimePointers = true; +}; + +} // namespace nativescript + +#endif // NATIVESCRIPT_FFI_SHARED_NATIVE_API_BACKEND_CONFIG_H diff --git a/NativeScript/jsi/shared/SignatureHashing.h b/NativeScript/jsi/shared/SignatureHashing.h new file mode 100644 index 000000000..0589c0fe3 --- /dev/null +++ b/NativeScript/jsi/shared/SignatureHashing.h @@ -0,0 +1,110 @@ +#ifndef NS_JSI_SHARED_SIGNATURE_HASHING_H +#define NS_JSI_SHARED_SIGNATURE_HASHING_H + +// Platform-neutral half of generated signature dispatch (GSD). +// +// This header must stay free of Objective-C and of any platform metadata +// format: it is compiled by the Apple runtime and, in future, by the Android +// runtime. The metadata-driven signature hashing (which walks the Objective-C +// metadata via metagen::MDMetadataReader) deliberately stays behind in +// ffi/objc/shared/SignatureDispatchCore.h, because the Android metadata is a +// different binary format entirely. +// +// What is genuinely common is the mechanism: hash a signature with FNV-1a, +// compose a dispatch id from (hash, call kind, flags), and binary-search a +// sorted, ahead-of-time generated table for a typed trampoline. + +#include +#include +#include + +namespace nativescript { + +enum class SignatureCallKind : uint8_t { + ObjCMethod = 1, + CFunction = 2, + BlockInvoke = 3, +}; + +using ObjCPreparedInvoker = void (*)(void* fnptr, void** avalues, + void* rvalue); +using CFunctionPreparedInvoker = void (*)(void* fnptr, void** avalues, + void* rvalue); +using BlockPreparedInvoker = void (*)(void* fnptr, void** avalues, + void* rvalue); + +struct ObjCDispatchEntry { + uint64_t dispatchId; + ObjCPreparedInvoker invoker; +}; + +struct CFunctionDispatchEntry { + uint64_t dispatchId; + CFunctionPreparedInvoker invoker; +}; + +struct BlockDispatchEntry { + uint64_t dispatchId; + BlockPreparedInvoker invoker; +}; + +inline constexpr uint64_t kSignatureHashOffsetBasis = 14695981039346656037ull; +inline constexpr uint64_t kSignatureHashPrime = 1099511628211ull; + +inline uint64_t hashBytesFnv1a(const void* data, size_t size, + uint64_t seed = kSignatureHashOffsetBasis) { + const auto* bytes = static_cast(data); + uint64_t hash = seed; + for (size_t i = 0; i < size; i++) { + hash ^= static_cast(bytes[i]); + hash *= kSignatureHashPrime; + } + return hash; +} + +inline uint64_t composeSignatureDispatchId(uint64_t signatureHash, + SignatureCallKind kind, + uint8_t flags) { + const uint8_t kindByte = static_cast(kind); + uint64_t hash = hashBytesFnv1a(&kindByte, sizeof(kindByte)); + hash = hashBytesFnv1a(&flags, sizeof(flags), hash); + return hashBytesFnv1a(&signatureHash, sizeof(signatureHash), hash); +} + +template +inline Invoker lookupDispatchInvoker(const Entry (&entries)[N], + uint64_t dispatchId) { + if (dispatchId == 0 || N <= 1) { + return nullptr; + } + + size_t low = 1; + size_t high = N; + while (low < high) { + const size_t mid = low + ((high - low) >> 1); + const uint64_t midId = entries[mid].dispatchId; + if (midId < dispatchId) { + low = mid + 1; + } else { + high = mid; + } + } + + if (low < N && entries[low].dispatchId == dispatchId) { + return entries[low].invoker; + } + return nullptr; +} + +inline bool isGeneratedDispatchEnabled() { + static const bool enabled = []() { + const char* disableFlag = std::getenv("NS_DISABLE_GSD"); + return disableFlag == nullptr || disableFlag[0] == '\0' || + (disableFlag[0] == '0' && disableFlag[1] == '\0'); + }(); + return enabled; +} + +} // namespace nativescript + +#endif // NS_JSI_SHARED_SIGNATURE_HASHING_H diff --git a/NativeScript/ffi/objc/shared/Tasks.cpp b/NativeScript/jsi/shared/Tasks.cpp similarity index 100% rename from NativeScript/ffi/objc/shared/Tasks.cpp rename to NativeScript/jsi/shared/Tasks.cpp diff --git a/NativeScript/jsi/shared/Tasks.h b/NativeScript/jsi/shared/Tasks.h new file mode 100644 index 000000000..cd3b4a68c --- /dev/null +++ b/NativeScript/jsi/shared/Tasks.h @@ -0,0 +1,20 @@ +#ifndef Tasks_h +#define Tasks_h + +#include +#include + +namespace nativescript { + +class Tasks { +public: + static void Register(std::function task); + static void Drain(); + static void ClearTasks(); +private: + static std::vector> tasks_; +}; + +} + +#endif /* Tasks_h */ diff --git a/scripts/check_jsi_layer_neutral.sh b/scripts/check_jsi_layer_neutral.sh new file mode 100755 index 000000000..ed5c6d9cb --- /dev/null +++ b/scripts/check_jsi_layer_neutral.sh @@ -0,0 +1,39 @@ +#!/bin/bash +# The shared JSI layer (NativeScript/jsi) is compiled by the Apple runtime today +# and by the Android runtime in future. Android has no Objective-C compiler and +# no Foundation framework, so a single stray #import there does not merely warn: +# it makes the file unbuildable on Android. +# +# This is easy to regress and hard to notice, because `#import +# ` compiles perfectly well under -x c++ on macOS. A +# green Apple build proves nothing. Hence this check. +set -e + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +LAYER="$ROOT/NativeScript/jsi" + +if [ ! -d "$LAYER" ]; then + echo "check_jsi_layer_neutral: $LAYER does not exist yet; nothing to check." + exit 0 +fi + +# Objective-C constructs, and platform headers that only exist on Apple. +PATTERN='#import|@interface|@implementation|@protocol|@selector|objc_msgSend|objc_get|/dev/null); then + echo "ERROR: Objective-C found in the shared JSI layer (NativeScript/jsi)." + echo "This code must compile on Android, which has no Objective-C compiler." + echo "Move the Apple-specific part back under NativeScript/ffi/objc/." + echo + echo "$MATCHES" + exit 1 +fi + +# .mm is Objective-C++ by definition and cannot build on Android. +if MM=$(find "$LAYER" -name '*.mm' -print 2>/dev/null | grep .); then + echo "ERROR: .mm sources in the shared JSI layer; these cannot build on Android:" + echo "$MM" + exit 1 +fi + +echo "check_jsi_layer_neutral: OK ($(find "$LAYER" -type f | wc -l | tr -d ' ') files, no Objective-C)" From f1df517e6da6d0af64452170463ad08b0fc5d0f8 Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Thu, 30 Jul 2026 22:42:57 +0500 Subject: [PATCH 2/7] refactor(jsi): move the QuickJS engine layer into the shared tree First engine through the shared-layer split, chosen as the pilot because it needed the least surgery: its contract header had two Objective-C references and its three implementation files had none. The `nativescript::engine` classes wrapping QuickJS -- Runtime, Value, Object, Function, Array, String, BigInt, ArrayBuffer and the host-object plumbing -- move to jsi/quickjs/ and become .cpp. They were already plain C++; the .mm extension was incidental, inherited from living under ffi/objc. The Apple bridge's baggage stays behind in ffi/objc/quickjs/, which now holds Foundation, , the Mach-O metadata section lookup, the metadata reader and the NativeApiClassBuilderProtocol forward declaration, then includes the neutral header. Apple sources include the same path as before, so no Apple include changed. The header's other platform includes (dispatch, dlfcn, ffi) turned out to be vestigial -- nothing in the header body used them. Proof this is genuinely neutral, rather than merely compiling on a Mac: the moved header passes -fsyntax-only as plain C++ with no Foundation in reach. That matters because #import compiles fine under -x c++ on macOS, so a green Apple build alone would prove nothing. Verified: iOS TestRunner on QuickJS, 713 specs, 0 failures. check_jsi_layer_neutral.sh passes. No Android file touched. (cherry picked from commit cf98d6ff6875ad83d5c54c177ed5bda94531cfd5) --- NativeScript/CMakeLists.txt | 8 +- .../ffi/objc/jsc/NativeApiJSCHostObjects.mm | 2 +- .../objc/quickjs/NativeApiQuickJSRuntime.h | 794 +---------------- .../ffi/objc/shared/RuntimeCleanupRegistry.h | 41 +- .../ffi/objc/v8/NativeApiV8HostObjects.mm | 2 +- .../quickjs/QuickJSHostObjects.cpp} | 2 +- .../quickjs/QuickJSRuntime.cpp} | 2 +- NativeScript/jsi/quickjs/QuickJSRuntime.h | 803 ++++++++++++++++++ .../quickjs/QuickJSValue.cpp} | 2 +- .../shared/NativeApiStackValueArray.h | 0 .../jsi/shared/RuntimeCleanupRegistry.h | 39 + 11 files changed, 868 insertions(+), 827 deletions(-) rename NativeScript/{ffi/objc/quickjs/NativeApiQuickJSHostObjects.mm => jsi/quickjs/QuickJSHostObjects.cpp} (99%) rename NativeScript/{ffi/objc/quickjs/NativeApiQuickJSRuntime.mm => jsi/quickjs/QuickJSRuntime.cpp} (96%) create mode 100644 NativeScript/jsi/quickjs/QuickJSRuntime.h rename NativeScript/{ffi/objc/quickjs/NativeApiQuickJSValue.mm => jsi/quickjs/QuickJSValue.cpp} (99%) rename NativeScript/{ffi/objc => jsi}/shared/NativeApiStackValueArray.h (100%) create mode 100644 NativeScript/jsi/shared/RuntimeCleanupRegistry.h diff --git a/NativeScript/CMakeLists.txt b/NativeScript/CMakeLists.txt index a34d840ec..9885d3f64 100644 --- a/NativeScript/CMakeLists.txt +++ b/NativeScript/CMakeLists.txt @@ -290,10 +290,12 @@ set(FFI_JSC_ENGINE_SOURCE_FILES set(FFI_QUICKJS_ENGINE_SOURCE_FILES ${FFI_ENGINE_SHARED_SOURCE_FILES} - ffi/objc/quickjs/NativeApiQuickJSHostObjects.mm + # Platform-neutral engine layer, shared with the Android runtime. + jsi/quickjs/QuickJSHostObjects.cpp + jsi/quickjs/QuickJSRuntime.cpp + jsi/quickjs/QuickJSValue.cpp + # Apple-only ObjC bridge on top of it. ffi/objc/quickjs/NativeApiQuickJS.mm - ffi/objc/quickjs/NativeApiQuickJSRuntime.mm - ffi/objc/quickjs/NativeApiQuickJSValue.mm ) set(SOURCE_FILES diff --git a/NativeScript/ffi/objc/jsc/NativeApiJSCHostObjects.mm b/NativeScript/ffi/objc/jsc/NativeApiJSCHostObjects.mm index 09d2ab3c6..282658d66 100644 --- a/NativeScript/ffi/objc/jsc/NativeApiJSCHostObjects.mm +++ b/NativeScript/ffi/objc/jsc/NativeApiJSCHostObjects.mm @@ -1,5 +1,5 @@ #include "NativeApiJSCRuntime.h" -#include "../shared/NativeApiStackValueArray.h" +#include "jsi/shared/NativeApiStackValueArray.h" #ifdef TARGET_ENGINE_JSC diff --git a/NativeScript/ffi/objc/quickjs/NativeApiQuickJSRuntime.h b/NativeScript/ffi/objc/quickjs/NativeApiQuickJSRuntime.h index 51facccf6..9f945f8e0 100644 --- a/NativeScript/ffi/objc/quickjs/NativeApiQuickJSRuntime.h +++ b/NativeScript/ffi/objc/quickjs/NativeApiQuickJSRuntime.h @@ -1,6 +1,16 @@ #ifndef NATIVESCRIPT_FFI_QUICKJS_NATIVE_API_QUICKJS_RUNTIME_H #define NATIVESCRIPT_FFI_QUICKJS_NATIVE_API_QUICKJS_RUNTIME_H +// Apple-side entry point for the QuickJS engine layer. +// +// The engine layer itself -- the `nativescript::engine` classes wrapping +// QuickJS -- moved to jsi/quickjs/QuickJSRuntime.h so the Android runtime can +// compile it too. What stays here is exactly what the Apple ObjC bridge needs +// on top of it and Android has no use for: Foundation, the Objective-C +// runtime, the Mach-O metadata section lookup, and the metadata reader. +// +// Apple sources include this path, unchanged, and get both halves. + #ifdef TARGET_ENGINE_QUICKJS #import @@ -11,31 +21,11 @@ #include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "../shared/RuntimeCleanupRegistry.h" #include "Metadata.h" #include "MetadataReader.h" #include "ffi.h" -#include "quickjs.h" + +#include "jsi/quickjs/QuickJSRuntime.h" @protocol NativeApiClassBuilderProtocol @end @@ -44,766 +34,6 @@ extern const unsigned char embedded_metadata[EMBED_METADATA_SIZE]; #endif -namespace nativescript { -namespace engine { - -class Runtime; -class Value; -class Object; -class Function; -class Array; -class String; -class BigInt; -class ArrayBuffer; - -class JSError : public std::runtime_error { - public: - JSError(Runtime&, const std::string& message) : std::runtime_error(message) {} - explicit JSError(const std::string& message) : std::runtime_error(message) {} -}; - -class StringBuffer { - public: - explicit StringBuffer(std::string value) : value_(std::move(value)) {} - const char* data() const { return value_.data(); } - size_t size() const { return value_.size(); } - - private: - std::string value_; -}; - -class MutableBuffer { - public: - virtual ~MutableBuffer() = default; - virtual size_t size() const = 0; - virtual uint8_t* data() = 0; -}; - -class PropNameID { - public: - PropNameID() = default; - explicit PropNameID(std::string value) : value_(std::move(value)) {} - static PropNameID forAscii(Runtime&, const char* value) { - return PropNameID(value != nullptr ? value : ""); - } - static PropNameID forAscii(Runtime&, const std::string& value) { return PropNameID(value); } - std::string utf8(Runtime&) const { return value_; } - - private: - std::string value_; -}; - -class HostObject { - public: - virtual ~HostObject() = default; - virtual Value get(Runtime& runtime, const PropNameID& name); - virtual bool set(Runtime& runtime, const PropNameID& name, const Value& value); - virtual std::vector getPropertyNames(Runtime& runtime); -}; - -using HostFunctionType = std::function; - -namespace quickjsengine { - -template -const void* hostObjectTypeToken() { - static int token = 0; - return &token; -} - -struct RuntimeState : RuntimeCleanupRegistry { - explicit RuntimeState(JSContext* context) : context(context) {} - JSContext* context = nullptr; - bool hostClassRegistered = false; - bool functionClassRegistered = false; - bool selectorGroupDataClassRegistered = false; -}; - -extern JSClassID gHostClassId; -extern JSClassID gFunctionClassId; - -std::shared_ptr stateForContext(JSContext* context); -void releaseStateForContext(JSContext* context); - -struct ValueStorage { - enum class Kind { - Undefined, - Null, - Bool, - Number, - QuickJS, - QuickJSBorrowed, - }; - - explicit ValueStorage(Kind kind) : kind(kind) {} - ~ValueStorage() { - if (kind == Kind::QuickJS && context != nullptr && !JS_IsUninitialized(value)) { - JS_FreeValue(context, value); - } - } - - Kind kind = Kind::Undefined; - bool boolValue = false; - double numberValue = 0; - JSContext* context = nullptr; - JSValue value = JS_UNINITIALIZED; -}; - -struct HostObjectHolder { - HostObjectHolder(std::shared_ptr state, std::shared_ptr hostObject, - const void* typeToken) - : state(std::move(state)), hostObject(std::move(hostObject)), typeToken(typeToken) {} - std::shared_ptr state; - std::shared_ptr hostObject; - const void* typeToken = nullptr; -}; - -struct FunctionHolder { - FunctionHolder(std::shared_ptr state, HostFunctionType callback) - : state(std::move(state)), callback(std::move(callback)) {} - std::shared_ptr state; - HostFunctionType callback; -}; - -struct ArrayBufferHolder { - explicit ArrayBufferHolder(std::shared_ptr buffer) : buffer(std::move(buffer)) {} - std::shared_ptr buffer; -}; - -inline std::string valueToUtf8(JSContext* context, JSValueConst value) { - size_t length = 0; - const char* cString = JS_ToCStringLen(context, &length, value); - if (cString == nullptr) { - return {}; - } - std::string result(cString, length); - JS_FreeCString(context, cString); - return result; -} - -inline std::string currentExceptionMessage(JSContext* context) { - JSValue exception = JS_GetException(context); - std::string message = valueToUtf8(context, exception); - JS_FreeValue(context, exception); - return message.empty() ? std::string("QuickJS function call failed.") - : message; -} - -inline std::string atomToUtf8(JSContext* context, JSAtom atom) { - const char* cString = JS_AtomToCString(context, atom); - if (cString == nullptr) { - return {}; - } - std::string result(cString); - JS_FreeCString(context, cString); - return result; -} - -inline JSValue throwError(JSContext* context, const std::exception& error) { - return JS_ThrowTypeError(context, "%s", error.what()); -} - -void ensureClasses(Runtime& runtime); - -} // namespace quickjsengine - -class Runtime { - public: - explicit Runtime(JSContext* context) : state_(quickjsengine::stateForContext(context)) {} - explicit Runtime(std::shared_ptr state) : state_(std::move(state)) {} - JSContext* context() const { return state_->context; } - std::shared_ptr state() const { return state_; } - const void* identity() const { return state_.get(); } - void detachState() { state_.reset(); } - Object global(); - Value evaluateJavaScript(std::shared_ptr buffer, const std::string& sourceURL); - void drainMicrotasks() { - JSContext* ctx = context(); - JSRuntime* rt = JS_GetRuntime(ctx); - JSContext* jobCtx = nullptr; - while (JS_ExecutePendingJob(rt, &jobCtx) > 0) { - } - } - - private: - std::shared_ptr state_; -}; - -class String { - public: - String() = default; - String(Runtime& runtime, JSValue value); - static String createFromUtf8(Runtime& runtime, const char* value) { - return String(runtime, JS_NewString(runtime.context(), value != nullptr ? value : "")); - } - static String createFromUtf8(Runtime& runtime, const std::string& value) { - return String(runtime, JS_NewStringLen(runtime.context(), value.data(), value.size())); - } - static String createFromUtf8(Runtime& runtime, const uint8_t* value, size_t length) { - return String(runtime, - JS_NewStringLen(runtime.context(), reinterpret_cast(value), length)); - } - std::string utf8(Runtime& runtime) const; - JSValue local(Runtime& runtime) const; - operator Value() const; - - private: - friend class Value; - std::shared_ptr storage_; -}; - -class Value { - public: - Value() : kind_(quickjsengine::ValueStorage::Kind::Undefined) {} - - Value(bool value) : kind_(quickjsengine::ValueStorage::Kind::Bool), boolValue_(value) {} - - Value(double value) : kind_(quickjsengine::ValueStorage::Kind::Number), numberValue_(value) {} - - Value(int value) : Value(static_cast(value)) {} - Value(uint32_t value) : Value(static_cast(value)) {} - - Value(Runtime& runtime, const Value& value) { - if (value.kind_ == quickjsengine::ValueStorage::Kind::QuickJSBorrowed) { - // Promote borrowed to owned - storage_ = std::make_shared( - quickjsengine::ValueStorage::Kind::QuickJS); - storage_->context = runtime.context(); - storage_->value = JS_DupValue(runtime.context(), value.borrowedValue_); - kind_ = quickjsengine::ValueStorage::Kind::QuickJS; - return; - } - kind_ = value.kind_; - boolValue_ = value.boolValue_; - numberValue_ = value.numberValue_; - borrowedContext_ = value.borrowedContext_; - borrowedValue_ = value.borrowedValue_; - storage_ = value.storage_; - } - Value(Runtime& runtime, Value&& value) - : kind_(value.kind_), - boolValue_(value.boolValue_), - numberValue_(value.numberValue_), - borrowedContext_(value.borrowedContext_), - borrowedValue_(value.borrowedValue_), - storage_(std::move(value.storage_)) {} - Value(Runtime& runtime, const String& value) : storage_(value.storage_) { - kind_ = storage_ ? storage_->kind : quickjsengine::ValueStorage::Kind::Undefined; - } - Value(Runtime& runtime, const Object& object); - Value(Runtime& runtime, const Function& function); - Value(Runtime& runtime, const Array& array); - Value(Runtime& runtime, const ArrayBuffer& arrayBuffer); - Value(Runtime& runtime, const BigInt& bigint); - Value(Runtime& runtime, JSValue value) - : kind_(quickjsengine::ValueStorage::Kind::QuickJS), - storage_(std::make_shared( - quickjsengine::ValueStorage::Kind::QuickJS)) { - storage_->context = runtime.context(); - storage_->value = JS_DupValue(runtime.context(), value); - } - - static Value borrowed(Runtime& runtime, JSValueConst value) { - Value result; - result.kind_ = quickjsengine::ValueStorage::Kind::QuickJSBorrowed; - result.borrowedContext_ = runtime.context(); - result.borrowedValue_ = value; - return result; - } - - static Value undefined() { return Value(); } - static Value null() { - Value value; - value.kind_ = quickjsengine::ValueStorage::Kind::Null; - return value; - } - static bool strictEquals(Runtime& runtime, const Value& lhs, - const Value& rhs) { - JSValue lhsValue = lhs.local(runtime); - JSValue rhsValue = rhs.local(runtime); - bool equal = JS_IsStrictEqual(runtime.context(), lhsValue, rhsValue); - JS_FreeValue(runtime.context(), lhsValue); - JS_FreeValue(runtime.context(), rhsValue); - return equal; - } - bool isUndefined() const { - if (kind_ == quickjsengine::ValueStorage::Kind::Undefined) { - return true; - } - return isQuickJS() && JS_IsUndefined(jsValue()); - } - bool isNull() const { - if (kind_ == quickjsengine::ValueStorage::Kind::Null) { - return true; - } - return isQuickJS() && JS_IsNull(jsValue()); - } - bool isBool() const { - if (kind_ == quickjsengine::ValueStorage::Kind::Bool) { - return true; - } - return isQuickJS() && JS_IsBool(jsValue()); - } - bool getBool() const { - if (kind_ == quickjsengine::ValueStorage::Kind::Bool) { - return boolValue_; - } - if (isQuickJS()) { - return JS_ToBool(jsContext(), jsValue()) != 0; - } - return false; - } - bool isNumber() const { - if (kind_ == quickjsengine::ValueStorage::Kind::Number) { - return true; - } - return isQuickJS() && JS_IsNumber(jsValue()); - } - double getNumber() const { - if (kind_ == quickjsengine::ValueStorage::Kind::Number) { - return numberValue_; - } - if (isQuickJS()) { - double value = 0; - JS_ToFloat64(jsContext(), &value, jsValue()); - return value; - } - return 0; - } - bool isObject() const { return isQuickJS() && JS_IsObject(jsValue()); } - bool isString() const { return isQuickJS() && JS_IsString(jsValue()); } - bool isBigInt() const { return isQuickJS() && JS_IsBigInt(jsValue()); } - bool isSymbol() const { return isQuickJS() && JS_IsSymbol(jsValue()); } - - Object asObject(Runtime& runtime) const; - String asString(Runtime& runtime) const; - BigInt getBigInt(Runtime& runtime) const; - - JSValue local(Runtime& runtime) const { - switch (kind_) { - case quickjsengine::ValueStorage::Kind::Undefined: - return JS_UNDEFINED; - case quickjsengine::ValueStorage::Kind::Null: - return JS_NULL; - case quickjsengine::ValueStorage::Kind::Bool: - return JS_NewBool(runtime.context(), boolValue_); - case quickjsengine::ValueStorage::Kind::Number: - return JS_NewFloat64(runtime.context(), numberValue_); - case quickjsengine::ValueStorage::Kind::QuickJS: - case quickjsengine::ValueStorage::Kind::QuickJSBorrowed: - return JS_DupValue(runtime.context(), jsValue()); - } - } - - // Access the shared storage (for Object/Function/Array interop) - std::shared_ptr storage() const { return storage_; } - - static Value fromStorage(std::shared_ptr s) { - Value v; - v.kind_ = s->kind; - v.boolValue_ = s->boolValue; - v.numberValue_ = s->numberValue; - v.storage_ = std::move(s); - return v; - } - - private: - friend class Runtime; - friend class Object; - friend class String; - friend class BigInt; - friend class ArrayBuffer; - friend class Function; - friend class Array; - - bool isQuickJS() const { - return kind_ == quickjsengine::ValueStorage::Kind::QuickJS || - kind_ == quickjsengine::ValueStorage::Kind::QuickJSBorrowed; - } - JSContext* jsContext() const { - return kind_ == quickjsengine::ValueStorage::Kind::QuickJSBorrowed ? borrowedContext_ - : storage_->context; - } - JSValue jsValue() const { - return kind_ == quickjsengine::ValueStorage::Kind::QuickJSBorrowed ? borrowedValue_ - : storage_->value; - } - - quickjsengine::ValueStorage::Kind kind_ = quickjsengine::ValueStorage::Kind::Undefined; - bool boolValue_ = false; - double numberValue_ = 0; - JSContext* borrowedContext_ = nullptr; - JSValue borrowedValue_ = JS_UNINITIALIZED; - std::shared_ptr storage_; -}; - -class Object { - public: - Object() = default; - explicit Object(Runtime& runtime) - : storage_(std::make_shared( - quickjsengine::ValueStorage::Kind::QuickJS)) { - storage_->context = runtime.context(); - storage_->value = JS_NewObject(runtime.context()); - } - static Object fromValueStorage(std::shared_ptr storage) { - Object object; - object.storage_ = std::move(storage); - return object; - } - template - static Object createFromHostObject(Runtime& runtime, std::shared_ptr host) { - auto baseHost = std::static_pointer_cast(std::move(host)); - return createFromHostObjectWithToken(runtime, std::move(baseHost), - quickjsengine::hostObjectTypeToken()); - } - - Value getProperty(Runtime& runtime, const char* name) const { - JSValue object = local(runtime); - JSValue result = JS_GetPropertyStr(runtime.context(), object, name != nullptr ? name : ""); - JS_FreeValue(runtime.context(), object); - if (JS_IsException(result)) { - throw JSError(runtime, "QuickJS property get failed."); - } - Value value(runtime, result); - JS_FreeValue(runtime.context(), result); - return value; - } - Value getProperty(Runtime& runtime, const std::string& name) const { - return getProperty(runtime, name.c_str()); - } - Value getProperty(Runtime& runtime, const Value& key) const { - JSValue object = local(runtime); - JSValue keyValue = key.local(runtime); - JSAtom atom = JS_ValueToAtom(runtime.context(), keyValue); - JS_FreeValue(runtime.context(), keyValue); - JSValue result = - atom == JS_ATOM_NULL ? JS_UNDEFINED : JS_GetProperty(runtime.context(), object, atom); - if (atom != JS_ATOM_NULL) { - JS_FreeAtom(runtime.context(), atom); - } - JS_FreeValue(runtime.context(), object); - if (JS_IsException(result)) { - throw JSError(runtime, "QuickJS property get failed."); - } - Value value(runtime, result); - JS_FreeValue(runtime.context(), result); - return value; - } - Object getPropertyAsObject(Runtime& runtime, const char* name) const { - return getProperty(runtime, name).asObject(runtime); - } - Function getPropertyAsFunction(Runtime& runtime, const char* name) const; - - void setProperty(Runtime& runtime, const char* name, const Value& value) { - JSValue object = local(runtime); - JSValue localValue = value.local(runtime); - int status = - JS_SetPropertyStr(runtime.context(), object, name != nullptr ? name : "", localValue); - JS_FreeValue(runtime.context(), object); - if (status < 0) { - throw JSError(runtime, "QuickJS property set failed."); - } - } - void setProperty(Runtime& runtime, const char* name, const String& value) { - setProperty(runtime, name, Value(runtime, value)); - } - void setProperty(Runtime& runtime, const char* name, const Object& value) { - setProperty(runtime, name, Value(runtime, value)); - } - void setProperty(Runtime& runtime, const char* name, const Function& value); - void setProperty(Runtime& runtime, const char* name, const Array& value); - void setProperty(Runtime& runtime, const char* name, const ArrayBuffer& value); - void setProperty(Runtime& runtime, const char* name, bool value) { - setProperty(runtime, name, Value(value)); - } - void setProperty(Runtime& runtime, const char* name, double value) { - setProperty(runtime, name, Value(value)); - } - void setProperty(Runtime& runtime, const std::string& name, const Value& value) { - setProperty(runtime, name.c_str(), value); - } - void setProperty(Runtime& runtime, const Value& key, const Value& value) { - JSValue object = local(runtime); - JSValue keyValue = key.local(runtime); - JSAtom atom = JS_ValueToAtom(runtime.context(), keyValue); - JS_FreeValue(runtime.context(), keyValue); - JSValue localValue = value.local(runtime); - int status = - atom == JS_ATOM_NULL ? -1 : JS_SetProperty(runtime.context(), object, atom, localValue); - if (atom != JS_ATOM_NULL) { - JS_FreeAtom(runtime.context(), atom); - } - JS_FreeValue(runtime.context(), object); - if (status < 0) { - throw JSError(runtime, "QuickJS property set failed."); - } - } - bool hasProperty(Runtime& runtime, const char* name) const { - JSValue object = local(runtime); - JSAtom atom = JS_NewAtom(runtime.context(), name != nullptr ? name : ""); - int result = JS_HasProperty(runtime.context(), object, atom); - JS_FreeAtom(runtime.context(), atom); - JS_FreeValue(runtime.context(), object); - return result > 0; - } - bool isFunction(Runtime& runtime) const { - JSValue object = local(runtime); - bool result = JS_IsFunction(runtime.context(), object); - JS_FreeValue(runtime.context(), object); - return result; - } - bool isArray(Runtime& runtime) const { - JSValue object = local(runtime); - int result = JS_IsArray(object); - JS_FreeValue(runtime.context(), object); - return result > 0; - } - bool isArrayBuffer(Runtime& runtime) const { - JSValue object = local(runtime); - bool result = JS_IsArrayBuffer(object); - JS_FreeValue(runtime.context(), object); - return result; - } - Function asFunction(Runtime& runtime) const; - Array getArray(Runtime& runtime) const; - ArrayBuffer getArrayBuffer(Runtime& runtime) const; - Array getPropertyNames(Runtime& runtime) const; - - template - bool isHostObject(Runtime& runtime) const { - auto holder = hostObjectHolder(runtime); - return holder != nullptr && holder->typeToken == quickjsengine::hostObjectTypeToken(); - } - template - std::shared_ptr getHostObject(Runtime& runtime) const { - auto holder = hostObjectHolder(runtime); - if (holder == nullptr || holder->typeToken != quickjsengine::hostObjectTypeToken()) { - return nullptr; - } - return std::static_pointer_cast(holder->hostObject); - } - JSValue local(Runtime& runtime) const { return JS_DupValue(runtime.context(), storage_->value); } - operator Value() const { return Value::fromStorage(storage_); } - - protected: - friend class Value; - friend class Runtime; - friend class Function; - friend class Array; - friend class ArrayBuffer; - explicit Object(std::shared_ptr storage) - : storage_(std::move(storage)) {} - static Object createFromHostObjectWithToken(Runtime& runtime, std::shared_ptr host, - const void* typeToken); - quickjsengine::HostObjectHolder* hostObjectHolder(Runtime& runtime) const; - std::shared_ptr storage_; -}; - -class Function : public Object { - public: - Function() = default; - explicit Function(Object object) : Object(std::move(object.storage_)) {} - static Function createFromHostFunction(Runtime& runtime, const PropNameID& name, unsigned int, - HostFunctionType callback); - Value call(Runtime& runtime, const Value* args, size_t count) const { - JSValue function = local(runtime); - JSValue global = JS_GetGlobalObject(runtime.context()); - std::vector argv; - argv.reserve(count); - for (size_t i = 0; i < count; i++) { - argv.push_back(args[i].local(runtime)); - } - JSValue result = JS_Call(runtime.context(), function, global, static_cast(argv.size()), - argv.empty() ? nullptr : argv.data()); - for (auto& arg : argv) { - JS_FreeValue(runtime.context(), arg); - } - JS_FreeValue(runtime.context(), global); - JS_FreeValue(runtime.context(), function); - if (JS_IsException(result)) { - throw JSError(runtime, quickjsengine::currentExceptionMessage(runtime.context())); - } - Value value(runtime, result); - JS_FreeValue(runtime.context(), result); - return value; - } - Value call(Runtime& runtime) const { - return call(runtime, static_cast(nullptr), 0); - } - Value call(Runtime& runtime, std::nullptr_t, size_t) const { - return call(runtime, static_cast(nullptr), 0); - } - template - Value call(Runtime& runtime, const Value (&args)[N], size_t count) const { - return call(runtime, static_cast(args), count); - } - template - Value call(Runtime& runtime, Args&&... args) const { - Value argv[] = {Value(runtime, std::forward(args))...}; - return call(runtime, static_cast(argv), sizeof...(Args)); - } - Value callWithThis(Runtime& runtime, const Object& thisObject, const Value* args = nullptr, - size_t count = 0) const { - JSValue function = local(runtime); - JSValue thisValue = thisObject.local(runtime); - std::vector argv; - argv.reserve(count); - for (size_t i = 0; i < count; i++) { - argv.push_back(args[i].local(runtime)); - } - JSValue result = JS_Call(runtime.context(), function, thisValue, static_cast(argv.size()), - argv.empty() ? nullptr : argv.data()); - for (auto& arg : argv) { - JS_FreeValue(runtime.context(), arg); - } - JS_FreeValue(runtime.context(), thisValue); - JS_FreeValue(runtime.context(), function); - if (JS_IsException(result)) { - throw JSError(runtime, quickjsengine::currentExceptionMessage(runtime.context())); - } - Value value(runtime, result); - JS_FreeValue(runtime.context(), result); - return value; - } - Value callAsConstructor(Runtime& runtime, const Value* args, size_t count) const { - JSValue function = local(runtime); - std::vector argv; - argv.reserve(count); - for (size_t i = 0; i < count; i++) { - argv.push_back(args[i].local(runtime)); - } - JSValue result = JS_CallConstructor(runtime.context(), function, static_cast(argv.size()), - argv.empty() ? nullptr : argv.data()); - for (auto& arg : argv) { - JS_FreeValue(runtime.context(), arg); - } - JS_FreeValue(runtime.context(), function); - if (JS_IsException(result)) { - throw JSError(runtime, "QuickJS constructor call failed."); - } - Value value(runtime, result); - JS_FreeValue(runtime.context(), result); - return value; - } - Value callAsConstructor(Runtime& runtime, std::nullptr_t, size_t) const { - return callAsConstructor(runtime, static_cast(nullptr), 0); - } - template - Value callAsConstructor(Runtime& runtime, const Value (&args)[N], size_t count) const { - return callAsConstructor(runtime, static_cast(args), count); - } - template - Value callAsConstructor(Runtime& runtime, Args&&... args) const { - Value argv[] = {Value(runtime, std::forward(args))...}; - return callAsConstructor(runtime, static_cast(argv), sizeof...(Args)); - } -}; - -class Array : public Object { - public: - explicit Array(Runtime& runtime, size_t size) - : Object(std::make_shared( - quickjsengine::ValueStorage::Kind::QuickJS)) { - storage_->context = runtime.context(); - storage_->value = JS_NewArray(runtime.context()); - JS_SetPropertyStr(runtime.context(), storage_->value, "length", - JS_NewUint32(runtime.context(), static_cast(size))); - } - explicit Array(Object object) : Object(std::move(object.storage_)) {} - size_t size(Runtime& runtime) const { - Value length = getProperty(runtime, "length"); - return length.isNumber() ? static_cast(std::max(0, length.getNumber())) : 0; - } - Value getValueAtIndex(Runtime& runtime, size_t index) const { - JSValue object = local(runtime); - JSValue result = JS_GetPropertyUint32(runtime.context(), object, static_cast(index)); - JS_FreeValue(runtime.context(), object); - if (JS_IsException(result)) { - throw JSError(runtime, "QuickJS array get failed."); - } - Value value(runtime, result); - JS_FreeValue(runtime.context(), result); - return value; - } - void setValueAtIndex(Runtime& runtime, size_t index, const Value& value) { - JSValue object = local(runtime); - JSValue localValue = value.local(runtime); - int status = - JS_SetPropertyUint32(runtime.context(), object, static_cast(index), localValue); - JS_FreeValue(runtime.context(), object); - if (status < 0) { - throw JSError(runtime, "QuickJS array set failed."); - } - } - void setValueAtIndex(Runtime& runtime, size_t index, const String& value) { - setValueAtIndex(runtime, index, Value(runtime, value)); - } -}; - -class BigInt { - public: - BigInt() = default; - BigInt(Runtime& runtime, JSValue value) - : storage_(std::make_shared( - quickjsengine::ValueStorage::Kind::QuickJS)) { - storage_->context = runtime.context(); - storage_->value = JS_DupValue(runtime.context(), value); - } - static BigInt fromInt64(Runtime& runtime, int64_t value) { - JSValue result = JS_NewBigInt64(runtime.context(), value); - BigInt bigint(runtime, result); - JS_FreeValue(runtime.context(), result); - return bigint; - } - static BigInt fromUint64(Runtime& runtime, uint64_t value) { - JSValue result = JS_NewBigUint64(runtime.context(), value); - BigInt bigint(runtime, result); - JS_FreeValue(runtime.context(), result); - return bigint; - } - String toString(Runtime& runtime, int) const; - JSValue local(Runtime& runtime) const { return JS_DupValue(runtime.context(), storage_->value); } - operator Value() const { return Value::fromStorage(storage_); } - - private: - friend class Value; - std::shared_ptr storage_; -}; - -class ArrayBuffer : public Object { - public: - ArrayBuffer(Runtime& runtime, std::shared_ptr buffer) - : Object(std::make_shared( - quickjsengine::ValueStorage::Kind::QuickJS)) { - auto* holder = new quickjsengine::ArrayBufferHolder(std::move(buffer)); - storage_->context = runtime.context(); - storage_->value = JS_NewArrayBuffer( - runtime.context(), holder->buffer->data(), holder->buffer->size(), - [](JSRuntime*, void* opaque, void*) { - delete static_cast(opaque); - }, - holder, false); - } - explicit ArrayBuffer(Object object) : Object(std::move(object.storage_)) {} - size_t size(Runtime& runtime) const { - JSValue object = local(runtime); - size_t size = 0; - JS_GetArrayBuffer(runtime.context(), &size, object); - JS_FreeValue(runtime.context(), object); - return size; - } - uint8_t* data(Runtime& runtime) const { - JSValue object = local(runtime); - size_t size = 0; - uint8_t* data = JS_GetArrayBuffer(runtime.context(), &size, object); - JS_FreeValue(runtime.context(), object); - return data; - } -}; -} // namespace engine -} // namespace nativescript - #endif // TARGET_ENGINE_QUICKJS #endif // NATIVESCRIPT_FFI_QUICKJS_NATIVE_API_QUICKJS_RUNTIME_H diff --git a/NativeScript/ffi/objc/shared/RuntimeCleanupRegistry.h b/NativeScript/ffi/objc/shared/RuntimeCleanupRegistry.h index d30928173..177950a62 100644 --- a/NativeScript/ffi/objc/shared/RuntimeCleanupRegistry.h +++ b/NativeScript/ffi/objc/shared/RuntimeCleanupRegistry.h @@ -1,39 +1,6 @@ -#ifndef NATIVESCRIPT_FFI_OBJC_RUNTIME_CLEANUP_REGISTRY_H -#define NATIVESCRIPT_FFI_OBJC_RUNTIME_CLEANUP_REGISTRY_H +#ifndef NATIVESCRIPT_FFI_OBJC_RUNTIME_CLEANUP_REGISTRY_FORWARD_H +#define NATIVESCRIPT_FFI_OBJC_RUNTIME_CLEANUP_REGISTRY_FORWARD_H -#include -#include +#include "jsi/shared/RuntimeCleanupRegistry.h" -namespace nativescript::engine { - -class RuntimeCleanupRegistry { - public: - using Cleanup = void (*)(void*); - - void track(void* pointer, Cleanup cleanup) { actions_[pointer] = cleanup; } - - template - void track(T* pointer) { - track(pointer, [](void* value) { delete static_cast(value); }); - } - - void untrack(void* pointer) { actions_.erase(pointer); } - bool empty() const { return actions_.empty(); } - - void cleanup() { - while (!actions_.empty()) { - auto action = actions_.begin(); - void* pointer = action->first; - Cleanup cleanup = action->second; - actions_.erase(action); - cleanup(pointer); - } - } - - private: - std::unordered_map actions_; -}; - -} // namespace nativescript::engine - -#endif // NATIVESCRIPT_FFI_OBJC_RUNTIME_CLEANUP_REGISTRY_H +#endif // NATIVESCRIPT_FFI_OBJC_RUNTIME_CLEANUP_REGISTRY_FORWARD_H diff --git a/NativeScript/ffi/objc/v8/NativeApiV8HostObjects.mm b/NativeScript/ffi/objc/v8/NativeApiV8HostObjects.mm index efa2af5a7..cd26fca47 100644 --- a/NativeScript/ffi/objc/v8/NativeApiV8HostObjects.mm +++ b/NativeScript/ffi/objc/v8/NativeApiV8HostObjects.mm @@ -1,5 +1,5 @@ #include "NativeApiV8Runtime.h" -#include "../shared/NativeApiStackValueArray.h" +#include "jsi/shared/NativeApiStackValueArray.h" #ifdef TARGET_ENGINE_V8 diff --git a/NativeScript/ffi/objc/quickjs/NativeApiQuickJSHostObjects.mm b/NativeScript/jsi/quickjs/QuickJSHostObjects.cpp similarity index 99% rename from NativeScript/ffi/objc/quickjs/NativeApiQuickJSHostObjects.mm rename to NativeScript/jsi/quickjs/QuickJSHostObjects.cpp index 2f38e3ffb..1bf35232f 100644 --- a/NativeScript/ffi/objc/quickjs/NativeApiQuickJSHostObjects.mm +++ b/NativeScript/jsi/quickjs/QuickJSHostObjects.cpp @@ -1,4 +1,4 @@ -#include "NativeApiQuickJSRuntime.h" +#include "jsi/quickjs/QuickJSRuntime.h" #include "../shared/NativeApiStackValueArray.h" #ifdef TARGET_ENGINE_QUICKJS diff --git a/NativeScript/ffi/objc/quickjs/NativeApiQuickJSRuntime.mm b/NativeScript/jsi/quickjs/QuickJSRuntime.cpp similarity index 96% rename from NativeScript/ffi/objc/quickjs/NativeApiQuickJSRuntime.mm rename to NativeScript/jsi/quickjs/QuickJSRuntime.cpp index d38eb3ac6..efdba2427 100644 --- a/NativeScript/ffi/objc/quickjs/NativeApiQuickJSRuntime.mm +++ b/NativeScript/jsi/quickjs/QuickJSRuntime.cpp @@ -1,4 +1,4 @@ -#include "NativeApiQuickJSRuntime.h" +#include "jsi/quickjs/QuickJSRuntime.h" #ifdef TARGET_ENGINE_QUICKJS diff --git a/NativeScript/jsi/quickjs/QuickJSRuntime.h b/NativeScript/jsi/quickjs/QuickJSRuntime.h new file mode 100644 index 000000000..5cd2d2b52 --- /dev/null +++ b/NativeScript/jsi/quickjs/QuickJSRuntime.h @@ -0,0 +1,803 @@ +#ifndef NATIVESCRIPT_JSI_QUICKJS_QUICKJS_RUNTIME_H +#define NATIVESCRIPT_JSI_QUICKJS_QUICKJS_RUNTIME_H + +// QuickJS behind the JSI-shaped `nativescript::engine` API. +// +// This header is platform-neutral: it depends on QuickJS and the C++ standard +// library, and on nothing from Foundation, the Objective-C runtime, or the +// Apple metadata format. Android compiles it as-is. +// +// The Apple bridge's extra baggage -- Foundation, , the Mach-O +// metadata section lookup, and the NativeApiClassBuilderProtocol forward +// declaration -- lives in ffi/objc/quickjs/NativeApiQuickJSRuntime.h, which +// includes this file. Apple sources keep including that path unchanged. + +#ifdef TARGET_ENGINE_QUICKJS + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "jsi/shared/RuntimeCleanupRegistry.h" +#include "quickjs.h" + +namespace nativescript { +namespace engine { + +class Runtime; +class Value; +class Object; +class Function; +class Array; +class String; +class BigInt; +class ArrayBuffer; + +class JSError : public std::runtime_error { + public: + JSError(Runtime&, const std::string& message) : std::runtime_error(message) {} + explicit JSError(const std::string& message) : std::runtime_error(message) {} +}; + +class StringBuffer { + public: + explicit StringBuffer(std::string value) : value_(std::move(value)) {} + const char* data() const { return value_.data(); } + size_t size() const { return value_.size(); } + + private: + std::string value_; +}; + +class MutableBuffer { + public: + virtual ~MutableBuffer() = default; + virtual size_t size() const = 0; + virtual uint8_t* data() = 0; +}; + +class PropNameID { + public: + PropNameID() = default; + explicit PropNameID(std::string value) : value_(std::move(value)) {} + static PropNameID forAscii(Runtime&, const char* value) { + return PropNameID(value != nullptr ? value : ""); + } + static PropNameID forAscii(Runtime&, const std::string& value) { return PropNameID(value); } + std::string utf8(Runtime&) const { return value_; } + + private: + std::string value_; +}; + +class HostObject { + public: + virtual ~HostObject() = default; + virtual Value get(Runtime& runtime, const PropNameID& name); + virtual bool set(Runtime& runtime, const PropNameID& name, const Value& value); + virtual std::vector getPropertyNames(Runtime& runtime); +}; + +using HostFunctionType = std::function; + +namespace quickjsengine { + +template +const void* hostObjectTypeToken() { + static int token = 0; + return &token; +} + +struct RuntimeState : RuntimeCleanupRegistry { + explicit RuntimeState(JSContext* context) : context(context) {} + JSContext* context = nullptr; + bool hostClassRegistered = false; + bool functionClassRegistered = false; + bool selectorGroupDataClassRegistered = false; +}; + +extern JSClassID gHostClassId; +extern JSClassID gFunctionClassId; + +std::shared_ptr stateForContext(JSContext* context); +void releaseStateForContext(JSContext* context); + +struct ValueStorage { + enum class Kind { + Undefined, + Null, + Bool, + Number, + QuickJS, + QuickJSBorrowed, + }; + + explicit ValueStorage(Kind kind) : kind(kind) {} + ~ValueStorage() { + if (kind == Kind::QuickJS && context != nullptr && !JS_IsUninitialized(value)) { + JS_FreeValue(context, value); + } + } + + Kind kind = Kind::Undefined; + bool boolValue = false; + double numberValue = 0; + JSContext* context = nullptr; + JSValue value = JS_UNINITIALIZED; +}; + +struct HostObjectHolder { + HostObjectHolder(std::shared_ptr state, std::shared_ptr hostObject, + const void* typeToken) + : state(std::move(state)), hostObject(std::move(hostObject)), typeToken(typeToken) {} + std::shared_ptr state; + std::shared_ptr hostObject; + const void* typeToken = nullptr; +}; + +struct FunctionHolder { + FunctionHolder(std::shared_ptr state, HostFunctionType callback) + : state(std::move(state)), callback(std::move(callback)) {} + std::shared_ptr state; + HostFunctionType callback; +}; + +struct ArrayBufferHolder { + explicit ArrayBufferHolder(std::shared_ptr buffer) : buffer(std::move(buffer)) {} + std::shared_ptr buffer; +}; + +inline std::string valueToUtf8(JSContext* context, JSValueConst value) { + size_t length = 0; + const char* cString = JS_ToCStringLen(context, &length, value); + if (cString == nullptr) { + return {}; + } + std::string result(cString, length); + JS_FreeCString(context, cString); + return result; +} + +inline std::string currentExceptionMessage(JSContext* context) { + JSValue exception = JS_GetException(context); + std::string message = valueToUtf8(context, exception); + JS_FreeValue(context, exception); + return message.empty() ? std::string("QuickJS function call failed.") + : message; +} + +inline std::string atomToUtf8(JSContext* context, JSAtom atom) { + const char* cString = JS_AtomToCString(context, atom); + if (cString == nullptr) { + return {}; + } + std::string result(cString); + JS_FreeCString(context, cString); + return result; +} + +inline JSValue throwError(JSContext* context, const std::exception& error) { + return JS_ThrowTypeError(context, "%s", error.what()); +} + +void ensureClasses(Runtime& runtime); + +} // namespace quickjsengine + +class Runtime { + public: + explicit Runtime(JSContext* context) : state_(quickjsengine::stateForContext(context)) {} + explicit Runtime(std::shared_ptr state) : state_(std::move(state)) {} + JSContext* context() const { return state_->context; } + std::shared_ptr state() const { return state_; } + const void* identity() const { return state_.get(); } + void detachState() { state_.reset(); } + Object global(); + Value evaluateJavaScript(std::shared_ptr buffer, const std::string& sourceURL); + void drainMicrotasks() { + JSContext* ctx = context(); + JSRuntime* rt = JS_GetRuntime(ctx); + JSContext* jobCtx = nullptr; + while (JS_ExecutePendingJob(rt, &jobCtx) > 0) { + } + } + + private: + std::shared_ptr state_; +}; + +class String { + public: + String() = default; + String(Runtime& runtime, JSValue value); + static String createFromUtf8(Runtime& runtime, const char* value) { + return String(runtime, JS_NewString(runtime.context(), value != nullptr ? value : "")); + } + static String createFromUtf8(Runtime& runtime, const std::string& value) { + return String(runtime, JS_NewStringLen(runtime.context(), value.data(), value.size())); + } + static String createFromUtf8(Runtime& runtime, const uint8_t* value, size_t length) { + return String(runtime, + JS_NewStringLen(runtime.context(), reinterpret_cast(value), length)); + } + std::string utf8(Runtime& runtime) const; + JSValue local(Runtime& runtime) const; + operator Value() const; + + private: + friend class Value; + std::shared_ptr storage_; +}; + +class Value { + public: + Value() : kind_(quickjsengine::ValueStorage::Kind::Undefined) {} + + Value(bool value) : kind_(quickjsengine::ValueStorage::Kind::Bool), boolValue_(value) {} + + Value(double value) : kind_(quickjsengine::ValueStorage::Kind::Number), numberValue_(value) {} + + Value(int value) : Value(static_cast(value)) {} + Value(uint32_t value) : Value(static_cast(value)) {} + + Value(Runtime& runtime, const Value& value) { + if (value.kind_ == quickjsengine::ValueStorage::Kind::QuickJSBorrowed) { + // Promote borrowed to owned + storage_ = std::make_shared( + quickjsengine::ValueStorage::Kind::QuickJS); + storage_->context = runtime.context(); + storage_->value = JS_DupValue(runtime.context(), value.borrowedValue_); + kind_ = quickjsengine::ValueStorage::Kind::QuickJS; + return; + } + kind_ = value.kind_; + boolValue_ = value.boolValue_; + numberValue_ = value.numberValue_; + borrowedContext_ = value.borrowedContext_; + borrowedValue_ = value.borrowedValue_; + storage_ = value.storage_; + } + Value(Runtime& runtime, Value&& value) + : kind_(value.kind_), + boolValue_(value.boolValue_), + numberValue_(value.numberValue_), + borrowedContext_(value.borrowedContext_), + borrowedValue_(value.borrowedValue_), + storage_(std::move(value.storage_)) {} + Value(Runtime& runtime, const String& value) : storage_(value.storage_) { + kind_ = storage_ ? storage_->kind : quickjsengine::ValueStorage::Kind::Undefined; + } + Value(Runtime& runtime, const Object& object); + Value(Runtime& runtime, const Function& function); + Value(Runtime& runtime, const Array& array); + Value(Runtime& runtime, const ArrayBuffer& arrayBuffer); + Value(Runtime& runtime, const BigInt& bigint); + Value(Runtime& runtime, JSValue value) + : kind_(quickjsengine::ValueStorage::Kind::QuickJS), + storage_(std::make_shared( + quickjsengine::ValueStorage::Kind::QuickJS)) { + storage_->context = runtime.context(); + storage_->value = JS_DupValue(runtime.context(), value); + } + + static Value borrowed(Runtime& runtime, JSValueConst value) { + Value result; + result.kind_ = quickjsengine::ValueStorage::Kind::QuickJSBorrowed; + result.borrowedContext_ = runtime.context(); + result.borrowedValue_ = value; + return result; + } + + static Value undefined() { return Value(); } + static Value null() { + Value value; + value.kind_ = quickjsengine::ValueStorage::Kind::Null; + return value; + } + static bool strictEquals(Runtime& runtime, const Value& lhs, + const Value& rhs) { + JSValue lhsValue = lhs.local(runtime); + JSValue rhsValue = rhs.local(runtime); + bool equal = JS_IsStrictEqual(runtime.context(), lhsValue, rhsValue); + JS_FreeValue(runtime.context(), lhsValue); + JS_FreeValue(runtime.context(), rhsValue); + return equal; + } + bool isUndefined() const { + if (kind_ == quickjsengine::ValueStorage::Kind::Undefined) { + return true; + } + return isQuickJS() && JS_IsUndefined(jsValue()); + } + bool isNull() const { + if (kind_ == quickjsengine::ValueStorage::Kind::Null) { + return true; + } + return isQuickJS() && JS_IsNull(jsValue()); + } + bool isBool() const { + if (kind_ == quickjsengine::ValueStorage::Kind::Bool) { + return true; + } + return isQuickJS() && JS_IsBool(jsValue()); + } + bool getBool() const { + if (kind_ == quickjsengine::ValueStorage::Kind::Bool) { + return boolValue_; + } + if (isQuickJS()) { + return JS_ToBool(jsContext(), jsValue()) != 0; + } + return false; + } + bool isNumber() const { + if (kind_ == quickjsengine::ValueStorage::Kind::Number) { + return true; + } + return isQuickJS() && JS_IsNumber(jsValue()); + } + double getNumber() const { + if (kind_ == quickjsengine::ValueStorage::Kind::Number) { + return numberValue_; + } + if (isQuickJS()) { + double value = 0; + JS_ToFloat64(jsContext(), &value, jsValue()); + return value; + } + return 0; + } + bool isObject() const { return isQuickJS() && JS_IsObject(jsValue()); } + bool isString() const { return isQuickJS() && JS_IsString(jsValue()); } + bool isBigInt() const { return isQuickJS() && JS_IsBigInt(jsValue()); } + bool isSymbol() const { return isQuickJS() && JS_IsSymbol(jsValue()); } + + Object asObject(Runtime& runtime) const; + String asString(Runtime& runtime) const; + BigInt getBigInt(Runtime& runtime) const; + + JSValue local(Runtime& runtime) const { + switch (kind_) { + case quickjsengine::ValueStorage::Kind::Undefined: + return JS_UNDEFINED; + case quickjsengine::ValueStorage::Kind::Null: + return JS_NULL; + case quickjsengine::ValueStorage::Kind::Bool: + return JS_NewBool(runtime.context(), boolValue_); + case quickjsengine::ValueStorage::Kind::Number: + return JS_NewFloat64(runtime.context(), numberValue_); + case quickjsengine::ValueStorage::Kind::QuickJS: + case quickjsengine::ValueStorage::Kind::QuickJSBorrowed: + return JS_DupValue(runtime.context(), jsValue()); + } + } + + // Access the shared storage (for Object/Function/Array interop) + std::shared_ptr storage() const { return storage_; } + + static Value fromStorage(std::shared_ptr s) { + Value v; + v.kind_ = s->kind; + v.boolValue_ = s->boolValue; + v.numberValue_ = s->numberValue; + v.storage_ = std::move(s); + return v; + } + + private: + friend class Runtime; + friend class Object; + friend class String; + friend class BigInt; + friend class ArrayBuffer; + friend class Function; + friend class Array; + + bool isQuickJS() const { + return kind_ == quickjsengine::ValueStorage::Kind::QuickJS || + kind_ == quickjsengine::ValueStorage::Kind::QuickJSBorrowed; + } + JSContext* jsContext() const { + return kind_ == quickjsengine::ValueStorage::Kind::QuickJSBorrowed ? borrowedContext_ + : storage_->context; + } + JSValue jsValue() const { + return kind_ == quickjsengine::ValueStorage::Kind::QuickJSBorrowed ? borrowedValue_ + : storage_->value; + } + + quickjsengine::ValueStorage::Kind kind_ = quickjsengine::ValueStorage::Kind::Undefined; + bool boolValue_ = false; + double numberValue_ = 0; + JSContext* borrowedContext_ = nullptr; + JSValue borrowedValue_ = JS_UNINITIALIZED; + std::shared_ptr storage_; +}; + +class Object { + public: + Object() = default; + explicit Object(Runtime& runtime) + : storage_(std::make_shared( + quickjsengine::ValueStorage::Kind::QuickJS)) { + storage_->context = runtime.context(); + storage_->value = JS_NewObject(runtime.context()); + } + static Object fromValueStorage(std::shared_ptr storage) { + Object object; + object.storage_ = std::move(storage); + return object; + } + template + static Object createFromHostObject(Runtime& runtime, std::shared_ptr host) { + auto baseHost = std::static_pointer_cast(std::move(host)); + return createFromHostObjectWithToken(runtime, std::move(baseHost), + quickjsengine::hostObjectTypeToken()); + } + + Value getProperty(Runtime& runtime, const char* name) const { + JSValue object = local(runtime); + JSValue result = JS_GetPropertyStr(runtime.context(), object, name != nullptr ? name : ""); + JS_FreeValue(runtime.context(), object); + if (JS_IsException(result)) { + throw JSError(runtime, "QuickJS property get failed."); + } + Value value(runtime, result); + JS_FreeValue(runtime.context(), result); + return value; + } + Value getProperty(Runtime& runtime, const std::string& name) const { + return getProperty(runtime, name.c_str()); + } + Value getProperty(Runtime& runtime, const Value& key) const { + JSValue object = local(runtime); + JSValue keyValue = key.local(runtime); + JSAtom atom = JS_ValueToAtom(runtime.context(), keyValue); + JS_FreeValue(runtime.context(), keyValue); + JSValue result = + atom == JS_ATOM_NULL ? JS_UNDEFINED : JS_GetProperty(runtime.context(), object, atom); + if (atom != JS_ATOM_NULL) { + JS_FreeAtom(runtime.context(), atom); + } + JS_FreeValue(runtime.context(), object); + if (JS_IsException(result)) { + throw JSError(runtime, "QuickJS property get failed."); + } + Value value(runtime, result); + JS_FreeValue(runtime.context(), result); + return value; + } + Object getPropertyAsObject(Runtime& runtime, const char* name) const { + return getProperty(runtime, name).asObject(runtime); + } + Function getPropertyAsFunction(Runtime& runtime, const char* name) const; + + void setProperty(Runtime& runtime, const char* name, const Value& value) { + JSValue object = local(runtime); + JSValue localValue = value.local(runtime); + int status = + JS_SetPropertyStr(runtime.context(), object, name != nullptr ? name : "", localValue); + JS_FreeValue(runtime.context(), object); + if (status < 0) { + throw JSError(runtime, "QuickJS property set failed."); + } + } + void setProperty(Runtime& runtime, const char* name, const String& value) { + setProperty(runtime, name, Value(runtime, value)); + } + void setProperty(Runtime& runtime, const char* name, const Object& value) { + setProperty(runtime, name, Value(runtime, value)); + } + void setProperty(Runtime& runtime, const char* name, const Function& value); + void setProperty(Runtime& runtime, const char* name, const Array& value); + void setProperty(Runtime& runtime, const char* name, const ArrayBuffer& value); + void setProperty(Runtime& runtime, const char* name, bool value) { + setProperty(runtime, name, Value(value)); + } + void setProperty(Runtime& runtime, const char* name, double value) { + setProperty(runtime, name, Value(value)); + } + void setProperty(Runtime& runtime, const std::string& name, const Value& value) { + setProperty(runtime, name.c_str(), value); + } + void setProperty(Runtime& runtime, const Value& key, const Value& value) { + JSValue object = local(runtime); + JSValue keyValue = key.local(runtime); + JSAtom atom = JS_ValueToAtom(runtime.context(), keyValue); + JS_FreeValue(runtime.context(), keyValue); + JSValue localValue = value.local(runtime); + int status = + atom == JS_ATOM_NULL ? -1 : JS_SetProperty(runtime.context(), object, atom, localValue); + if (atom != JS_ATOM_NULL) { + JS_FreeAtom(runtime.context(), atom); + } + JS_FreeValue(runtime.context(), object); + if (status < 0) { + throw JSError(runtime, "QuickJS property set failed."); + } + } + bool hasProperty(Runtime& runtime, const char* name) const { + JSValue object = local(runtime); + JSAtom atom = JS_NewAtom(runtime.context(), name != nullptr ? name : ""); + int result = JS_HasProperty(runtime.context(), object, atom); + JS_FreeAtom(runtime.context(), atom); + JS_FreeValue(runtime.context(), object); + return result > 0; + } + bool isFunction(Runtime& runtime) const { + JSValue object = local(runtime); + bool result = JS_IsFunction(runtime.context(), object); + JS_FreeValue(runtime.context(), object); + return result; + } + bool isArray(Runtime& runtime) const { + JSValue object = local(runtime); + int result = JS_IsArray(object); + JS_FreeValue(runtime.context(), object); + return result > 0; + } + bool isArrayBuffer(Runtime& runtime) const { + JSValue object = local(runtime); + bool result = JS_IsArrayBuffer(object); + JS_FreeValue(runtime.context(), object); + return result; + } + Function asFunction(Runtime& runtime) const; + Array getArray(Runtime& runtime) const; + ArrayBuffer getArrayBuffer(Runtime& runtime) const; + Array getPropertyNames(Runtime& runtime) const; + + template + bool isHostObject(Runtime& runtime) const { + auto holder = hostObjectHolder(runtime); + return holder != nullptr && holder->typeToken == quickjsengine::hostObjectTypeToken(); + } + template + std::shared_ptr getHostObject(Runtime& runtime) const { + auto holder = hostObjectHolder(runtime); + if (holder == nullptr || holder->typeToken != quickjsengine::hostObjectTypeToken()) { + return nullptr; + } + return std::static_pointer_cast(holder->hostObject); + } + JSValue local(Runtime& runtime) const { return JS_DupValue(runtime.context(), storage_->value); } + operator Value() const { return Value::fromStorage(storage_); } + + protected: + friend class Value; + friend class Runtime; + friend class Function; + friend class Array; + friend class ArrayBuffer; + explicit Object(std::shared_ptr storage) + : storage_(std::move(storage)) {} + static Object createFromHostObjectWithToken(Runtime& runtime, std::shared_ptr host, + const void* typeToken); + quickjsengine::HostObjectHolder* hostObjectHolder(Runtime& runtime) const; + std::shared_ptr storage_; +}; + +class Function : public Object { + public: + Function() = default; + explicit Function(Object object) : Object(std::move(object.storage_)) {} + static Function createFromHostFunction(Runtime& runtime, const PropNameID& name, unsigned int, + HostFunctionType callback); + Value call(Runtime& runtime, const Value* args, size_t count) const { + JSValue function = local(runtime); + JSValue global = JS_GetGlobalObject(runtime.context()); + std::vector argv; + argv.reserve(count); + for (size_t i = 0; i < count; i++) { + argv.push_back(args[i].local(runtime)); + } + JSValue result = JS_Call(runtime.context(), function, global, static_cast(argv.size()), + argv.empty() ? nullptr : argv.data()); + for (auto& arg : argv) { + JS_FreeValue(runtime.context(), arg); + } + JS_FreeValue(runtime.context(), global); + JS_FreeValue(runtime.context(), function); + if (JS_IsException(result)) { + throw JSError(runtime, quickjsengine::currentExceptionMessage(runtime.context())); + } + Value value(runtime, result); + JS_FreeValue(runtime.context(), result); + return value; + } + Value call(Runtime& runtime) const { + return call(runtime, static_cast(nullptr), 0); + } + Value call(Runtime& runtime, std::nullptr_t, size_t) const { + return call(runtime, static_cast(nullptr), 0); + } + template + Value call(Runtime& runtime, const Value (&args)[N], size_t count) const { + return call(runtime, static_cast(args), count); + } + template + Value call(Runtime& runtime, Args&&... args) const { + Value argv[] = {Value(runtime, std::forward(args))...}; + return call(runtime, static_cast(argv), sizeof...(Args)); + } + Value callWithThis(Runtime& runtime, const Object& thisObject, const Value* args = nullptr, + size_t count = 0) const { + JSValue function = local(runtime); + JSValue thisValue = thisObject.local(runtime); + std::vector argv; + argv.reserve(count); + for (size_t i = 0; i < count; i++) { + argv.push_back(args[i].local(runtime)); + } + JSValue result = JS_Call(runtime.context(), function, thisValue, static_cast(argv.size()), + argv.empty() ? nullptr : argv.data()); + for (auto& arg : argv) { + JS_FreeValue(runtime.context(), arg); + } + JS_FreeValue(runtime.context(), thisValue); + JS_FreeValue(runtime.context(), function); + if (JS_IsException(result)) { + throw JSError(runtime, quickjsengine::currentExceptionMessage(runtime.context())); + } + Value value(runtime, result); + JS_FreeValue(runtime.context(), result); + return value; + } + Value callAsConstructor(Runtime& runtime, const Value* args, size_t count) const { + JSValue function = local(runtime); + std::vector argv; + argv.reserve(count); + for (size_t i = 0; i < count; i++) { + argv.push_back(args[i].local(runtime)); + } + JSValue result = JS_CallConstructor(runtime.context(), function, static_cast(argv.size()), + argv.empty() ? nullptr : argv.data()); + for (auto& arg : argv) { + JS_FreeValue(runtime.context(), arg); + } + JS_FreeValue(runtime.context(), function); + if (JS_IsException(result)) { + throw JSError(runtime, "QuickJS constructor call failed."); + } + Value value(runtime, result); + JS_FreeValue(runtime.context(), result); + return value; + } + Value callAsConstructor(Runtime& runtime, std::nullptr_t, size_t) const { + return callAsConstructor(runtime, static_cast(nullptr), 0); + } + template + Value callAsConstructor(Runtime& runtime, const Value (&args)[N], size_t count) const { + return callAsConstructor(runtime, static_cast(args), count); + } + template + Value callAsConstructor(Runtime& runtime, Args&&... args) const { + Value argv[] = {Value(runtime, std::forward(args))...}; + return callAsConstructor(runtime, static_cast(argv), sizeof...(Args)); + } +}; + +class Array : public Object { + public: + explicit Array(Runtime& runtime, size_t size) + : Object(std::make_shared( + quickjsengine::ValueStorage::Kind::QuickJS)) { + storage_->context = runtime.context(); + storage_->value = JS_NewArray(runtime.context()); + JS_SetPropertyStr(runtime.context(), storage_->value, "length", + JS_NewUint32(runtime.context(), static_cast(size))); + } + explicit Array(Object object) : Object(std::move(object.storage_)) {} + size_t size(Runtime& runtime) const { + Value length = getProperty(runtime, "length"); + return length.isNumber() ? static_cast(std::max(0, length.getNumber())) : 0; + } + Value getValueAtIndex(Runtime& runtime, size_t index) const { + JSValue object = local(runtime); + JSValue result = JS_GetPropertyUint32(runtime.context(), object, static_cast(index)); + JS_FreeValue(runtime.context(), object); + if (JS_IsException(result)) { + throw JSError(runtime, "QuickJS array get failed."); + } + Value value(runtime, result); + JS_FreeValue(runtime.context(), result); + return value; + } + void setValueAtIndex(Runtime& runtime, size_t index, const Value& value) { + JSValue object = local(runtime); + JSValue localValue = value.local(runtime); + int status = + JS_SetPropertyUint32(runtime.context(), object, static_cast(index), localValue); + JS_FreeValue(runtime.context(), object); + if (status < 0) { + throw JSError(runtime, "QuickJS array set failed."); + } + } + void setValueAtIndex(Runtime& runtime, size_t index, const String& value) { + setValueAtIndex(runtime, index, Value(runtime, value)); + } +}; + +class BigInt { + public: + BigInt() = default; + BigInt(Runtime& runtime, JSValue value) + : storage_(std::make_shared( + quickjsengine::ValueStorage::Kind::QuickJS)) { + storage_->context = runtime.context(); + storage_->value = JS_DupValue(runtime.context(), value); + } + static BigInt fromInt64(Runtime& runtime, int64_t value) { + JSValue result = JS_NewBigInt64(runtime.context(), value); + BigInt bigint(runtime, result); + JS_FreeValue(runtime.context(), result); + return bigint; + } + static BigInt fromUint64(Runtime& runtime, uint64_t value) { + JSValue result = JS_NewBigUint64(runtime.context(), value); + BigInt bigint(runtime, result); + JS_FreeValue(runtime.context(), result); + return bigint; + } + String toString(Runtime& runtime, int) const; + JSValue local(Runtime& runtime) const { return JS_DupValue(runtime.context(), storage_->value); } + operator Value() const { return Value::fromStorage(storage_); } + + private: + friend class Value; + std::shared_ptr storage_; +}; + +class ArrayBuffer : public Object { + public: + ArrayBuffer(Runtime& runtime, std::shared_ptr buffer) + : Object(std::make_shared( + quickjsengine::ValueStorage::Kind::QuickJS)) { + auto* holder = new quickjsengine::ArrayBufferHolder(std::move(buffer)); + storage_->context = runtime.context(); + storage_->value = JS_NewArrayBuffer( + runtime.context(), holder->buffer->data(), holder->buffer->size(), + [](JSRuntime*, void* opaque, void*) { + delete static_cast(opaque); + }, + holder, false); + } + explicit ArrayBuffer(Object object) : Object(std::move(object.storage_)) {} + size_t size(Runtime& runtime) const { + JSValue object = local(runtime); + size_t size = 0; + JS_GetArrayBuffer(runtime.context(), &size, object); + JS_FreeValue(runtime.context(), object); + return size; + } + uint8_t* data(Runtime& runtime) const { + JSValue object = local(runtime); + size_t size = 0; + uint8_t* data = JS_GetArrayBuffer(runtime.context(), &size, object); + JS_FreeValue(runtime.context(), object); + return data; + } +}; +} // namespace engine +} // namespace nativescript + +#endif // TARGET_ENGINE_QUICKJS + +#endif // NATIVESCRIPT_JSI_QUICKJS_QUICKJS_RUNTIME_H diff --git a/NativeScript/ffi/objc/quickjs/NativeApiQuickJSValue.mm b/NativeScript/jsi/quickjs/QuickJSValue.cpp similarity index 99% rename from NativeScript/ffi/objc/quickjs/NativeApiQuickJSValue.mm rename to NativeScript/jsi/quickjs/QuickJSValue.cpp index bb6852019..8bfb7ebcc 100644 --- a/NativeScript/ffi/objc/quickjs/NativeApiQuickJSValue.mm +++ b/NativeScript/jsi/quickjs/QuickJSValue.cpp @@ -1,4 +1,4 @@ -#include "NativeApiQuickJSRuntime.h" +#include "jsi/quickjs/QuickJSRuntime.h" #ifdef TARGET_ENGINE_QUICKJS diff --git a/NativeScript/ffi/objc/shared/NativeApiStackValueArray.h b/NativeScript/jsi/shared/NativeApiStackValueArray.h similarity index 100% rename from NativeScript/ffi/objc/shared/NativeApiStackValueArray.h rename to NativeScript/jsi/shared/NativeApiStackValueArray.h diff --git a/NativeScript/jsi/shared/RuntimeCleanupRegistry.h b/NativeScript/jsi/shared/RuntimeCleanupRegistry.h new file mode 100644 index 000000000..eb2f9d316 --- /dev/null +++ b/NativeScript/jsi/shared/RuntimeCleanupRegistry.h @@ -0,0 +1,39 @@ +#ifndef NATIVESCRIPT_JSI_SHARED_RUNTIME_CLEANUP_REGISTRY_H +#define NATIVESCRIPT_JSI_SHARED_RUNTIME_CLEANUP_REGISTRY_H + +#include +#include + +namespace nativescript::engine { + +class RuntimeCleanupRegistry { + public: + using Cleanup = void (*)(void*); + + void track(void* pointer, Cleanup cleanup) { actions_[pointer] = cleanup; } + + template + void track(T* pointer) { + track(pointer, [](void* value) { delete static_cast(value); }); + } + + void untrack(void* pointer) { actions_.erase(pointer); } + bool empty() const { return actions_.empty(); } + + void cleanup() { + while (!actions_.empty()) { + auto action = actions_.begin(); + void* pointer = action->first; + Cleanup cleanup = action->second; + actions_.erase(action); + cleanup(pointer); + } + } + + private: + std::unordered_map actions_; +}; + +} // namespace nativescript::engine + +#endif // NATIVESCRIPT_JSI_SHARED_RUNTIME_CLEANUP_REGISTRY_H From 07806fe32a35fb66a8ebf20b52eec85661d1f2e8 Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Thu, 30 Jul 2026 23:15:35 +0500 Subject: [PATCH 3/7] refactor(jsi): move the V8 engine layer into the shared tree Same split as QuickJS: the `nativescript::engine` classes wrapping V8 move to jsi/v8/ as .cpp, the Apple bridge's Foundation/objc/Mach-O/metadata preamble stays behind in a header at the original path that includes the neutral one. No Apple include changed. V8 is the first engine whose RuntimeState was not already neutral. Its argument-marshalling caches -- which memoise "this JS value denotes that native type / method" -- stored Class and SEL directly. They now hold const void*, with the casts moved into NativeApiV8Marshalling.mm where the rest of the Objective-C already lives. That is a generalisation rather than a workaround: the Android runtime needs exactly the same cache holding jclass and jmethodID, and all four of these are pointers. Making the slot opaque is what lets one cache serve both bridges. Verified: iOS TestRunner on V8, 713 specs, 0 failures. The neutral header also passes -fsyntax-only as plain C++ with no Foundation in reach. (cherry picked from commit d40017efec63ed0dc8b5384b30663a8e9a81effb) --- NativeScript/CMakeLists.txt | 8 +- .../ffi/objc/v8/NativeApiV8Marshalling.mm | 14 +- NativeScript/ffi/objc/v8/NativeApiV8Runtime.h | 833 +---------------- .../v8/V8HostObjects.cpp} | 2 +- .../v8/V8Runtime.cpp} | 2 +- NativeScript/jsi/v8/V8Runtime.h | 851 ++++++++++++++++++ .../v8/V8Value.cpp} | 2 +- 7 files changed, 879 insertions(+), 833 deletions(-) rename NativeScript/{ffi/objc/v8/NativeApiV8HostObjects.mm => jsi/v8/V8HostObjects.cpp} (99%) rename NativeScript/{ffi/objc/v8/NativeApiV8Runtime.mm => jsi/v8/V8Runtime.cpp} (97%) create mode 100644 NativeScript/jsi/v8/V8Runtime.h rename NativeScript/{ffi/objc/v8/NativeApiV8Value.mm => jsi/v8/V8Value.cpp} (99%) diff --git a/NativeScript/CMakeLists.txt b/NativeScript/CMakeLists.txt index 9885d3f64..99078b8bd 100644 --- a/NativeScript/CMakeLists.txt +++ b/NativeScript/CMakeLists.txt @@ -274,10 +274,12 @@ set(FFI_HERMES_ENGINE_SOURCE_FILES set(FFI_V8_ENGINE_SOURCE_FILES ${FFI_ENGINE_SHARED_SOURCE_FILES} + # Platform-neutral engine layer, shared with the Android runtime. + jsi/v8/V8HostObjects.cpp + jsi/v8/V8Runtime.cpp + jsi/v8/V8Value.cpp + # Apple-only ObjC bridge on top of it. ffi/objc/v8/NativeApiV8.mm - ffi/objc/v8/NativeApiV8HostObjects.mm - ffi/objc/v8/NativeApiV8Runtime.mm - ffi/objc/v8/NativeApiV8Value.mm ) set(FFI_JSC_ENGINE_SOURCE_FILES diff --git a/NativeScript/ffi/objc/v8/NativeApiV8Marshalling.mm b/NativeScript/ffi/objc/v8/NativeApiV8Marshalling.mm index 97ef47828..1b410907b 100644 --- a/NativeScript/ffi/objc/v8/NativeApiV8Marshalling.mm +++ b/NativeScript/ffi/objc/v8/NativeApiV8Marshalling.mm @@ -124,17 +124,19 @@ Class v8NativeClassArgument(Runtime& runtime, v8::Local value) { } auto* state = runtime.rawState(); if (state != nullptr && value->IsObject()) { - if (state->nativeClassArgumentLast.nativeClass != Nil && + // The cache holds the Class opaquely so the engine layer stays platform + // neutral; cast back on the way out. + if (state->nativeClassArgumentLast.nativeClass != nullptr && !state->nativeClassArgumentLast.value.IsEmpty() && state->nativeClassArgumentLast.value.Get(runtime.isolate()) == value) { - return state->nativeClassArgumentLast.nativeClass; + return (Class)state->nativeClassArgumentLast.nativeClass; } for (auto& entry : state->nativeClassArgumentCache) { - if (entry.nativeClass != Nil && !entry.value.IsEmpty() && + if (entry.nativeClass != nullptr && !entry.value.IsEmpty() && entry.value.Get(runtime.isolate()) == value) { state->nativeClassArgumentLast.value.Reset(runtime.isolate(), value); state->nativeClassArgumentLast.nativeClass = entry.nativeClass; - return entry.nativeClass; + return (Class)entry.nativeClass; } } } @@ -194,13 +196,13 @@ bool readV8EngineSelectorArgument(Runtime& runtime, v8::Local value, !state->nativeSelectorArgumentLast.value.IsEmpty() && state->nativeSelectorArgumentLast.value.Get(runtime.isolate()) == value) { - *result = state->nativeSelectorArgumentLast.selector; + *result = (SEL)state->nativeSelectorArgumentLast.selector; return true; } for (auto& entry : state->nativeSelectorArgumentCache) { if (entry.selector != nullptr && !entry.value.IsEmpty() && entry.value.Get(runtime.isolate()) == value) { - *result = entry.selector; + *result = (SEL)entry.selector; state->nativeSelectorArgumentLast.value.Reset(runtime.isolate(), value); state->nativeSelectorArgumentLast.selector = entry.selector; return true; diff --git a/NativeScript/ffi/objc/v8/NativeApiV8Runtime.h b/NativeScript/ffi/objc/v8/NativeApiV8Runtime.h index 9dcd60ed1..67c59063d 100644 --- a/NativeScript/ffi/objc/v8/NativeApiV8Runtime.h +++ b/NativeScript/ffi/objc/v8/NativeApiV8Runtime.h @@ -1,6 +1,16 @@ #ifndef NATIVESCRIPT_FFI_V8_NATIVE_API_V8_RUNTIME_H #define NATIVESCRIPT_FFI_V8_NATIVE_API_V8_RUNTIME_H +// Apple-side entry point for the V8 engine layer. +// +// The engine layer itself -- the `nativescript::engine` classes wrapping V8 -- +// moved to jsi/v8/V8Runtime.h so the Android runtime can compile it too. What +// stays here is exactly what the Apple ObjC bridge needs on top of it and +// Android has no use for: Foundation, the Objective-C runtime, the Mach-O +// metadata section lookup, and the metadata reader. +// +// Apple sources include this path, unchanged, and get both halves. + #ifdef TARGET_ENGINE_V8 #import @@ -11,31 +21,11 @@ #include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "../shared/RuntimeCleanupRegistry.h" #include "Metadata.h" #include "MetadataReader.h" #include "ffi.h" -#include "v8.h" + +#include "jsi/v8/V8Runtime.h" @protocol NativeApiClassBuilderProtocol @end @@ -44,805 +34,6 @@ extern const unsigned char embedded_metadata[EMBED_METADATA_SIZE]; #endif -namespace nativescript { -namespace engine { - -class Runtime; -class Value; -class Object; -class Function; -class Array; -class String; -class BigInt; -class ArrayBuffer; - -class JSError : public std::runtime_error { - public: - JSError(Runtime&, const std::string& message) : std::runtime_error(message) {} - explicit JSError(const std::string& message) : std::runtime_error(message) {} -}; - -class StringBuffer { - public: - explicit StringBuffer(std::string value) : value_(std::move(value)) {} - const char* data() const { return value_.data(); } - size_t size() const { return value_.size(); } - - private: - std::string value_; -}; - -class MutableBuffer { - public: - virtual ~MutableBuffer() = default; - virtual size_t size() const = 0; - virtual uint8_t* data() = 0; -}; - -class PropNameID { - public: - PropNameID() = default; - explicit PropNameID(std::string value) : value_(std::move(value)) {} - - static PropNameID forAscii(Runtime&, const char* value) { - return PropNameID(value != nullptr ? value : ""); - } - - static PropNameID forAscii(Runtime&, const std::string& value) { return PropNameID(value); } - - std::string utf8(Runtime&) const { return value_; } - - private: - std::string value_; -}; - -class HostObject { - public: - virtual ~HostObject() = default; - virtual Value get(Runtime& runtime, const PropNameID& name); - virtual bool set(Runtime& runtime, const PropNameID& name, const Value& value); - virtual std::vector getPropertyNames(Runtime& runtime); -}; - -using HostFunctionType = std::function; - -namespace v8engine { - -struct RuntimeState { - RuntimeState(v8::Isolate* isolate, v8::Local context) - : isolate(isolate), context(isolate, context) {} - - ~RuntimeState() { - nativeClassArgumentLast.value.Reset(); - nativeClassArgumentLast.nativeClass = Nil; - for (auto& entry : nativeClassArgumentCache) { - entry.value.Reset(); - entry.nativeClass = Nil; - } - nativeSelectorArgumentLast.value.Reset(); - nativeSelectorArgumentLast.selector = nullptr; - for (auto& entry : nativeSelectorArgumentCache) { - entry.value.Reset(); - entry.selector = nullptr; - } - context.Reset(); - } - - v8::Local localContext() const { - return context.Get(isolate); - } - - v8::Isolate* isolate = nullptr; - v8::Global context; - v8::Global hostObjectTemplate; - v8::Global nativeObjectTemplate; // kNonMasking for instances - struct NativeClassArgumentCacheEntry { - v8::Global value; - Class nativeClass = Nil; - }; - NativeClassArgumentCacheEntry nativeClassArgumentLast; - NativeClassArgumentCacheEntry nativeClassArgumentCache[4]; - size_t nativeClassArgumentCacheNext = 0; - struct NativeSelectorArgumentCacheEntry { - v8::Global value; - SEL selector = nullptr; - }; - NativeSelectorArgumentCacheEntry nativeSelectorArgumentLast; - NativeSelectorArgumentCacheEntry nativeSelectorArgumentCache[4]; - size_t nativeSelectorArgumentCacheNext = 0; -}; - -struct ValueStorage { - enum class Kind : uint8_t { - Undefined, - Null, - Bool, - Number, - V8, - V8Borrowed, - }; - - explicit ValueStorage(Kind kind) : kind(kind) {} - - ~ValueStorage() { value.Reset(); } - - Kind kind = Kind::Undefined; - bool boolValue = false; - double numberValue = 0; - v8::Global value; - v8::Local borrowedValue; -}; - -template -const void* hostObjectTypeToken() { - static int token = 0; - return &token; -} - -struct HostObjectHolder { - HostObjectHolder(std::shared_ptr state, std::shared_ptr hostObject, - const void* typeToken) - : state(std::move(state)), hostObject(std::move(hostObject)), typeToken(typeToken) {} - - ~HostObjectHolder() { object.Reset(); } - - std::shared_ptr state; - std::shared_ptr hostObject; - const void* typeToken = nullptr; - v8::Global object; -}; - -struct FunctionHolder { - FunctionHolder(std::shared_ptr state, HostFunctionType callback) - : state(std::move(state)), callback(std::move(callback)) {} - - ~FunctionHolder() { function.Reset(); } - - std::shared_ptr state; - HostFunctionType callback; - v8::Global function; -}; - -inline thread_local std::unordered_map - runtimeAllocations; - -template -void trackRuntimeAllocation(v8::Isolate* isolate, T* allocation) { - runtimeAllocations[isolate].track(allocation); -} - -inline void untrackRuntimeAllocation(v8::Isolate* isolate, - void* allocation) { - auto runtime = runtimeAllocations.find(isolate); - if (runtime == runtimeAllocations.end()) { - return; - } - runtime->second.untrack(allocation); - if (runtime->second.empty()) { - runtimeAllocations.erase(runtime); - } -} - -inline void cleanupRuntimeAllocations(v8::Isolate* isolate) { - auto runtime = runtimeAllocations.find(isolate); - if (runtime == runtimeAllocations.end()) { - return; - } - - runtime->second.cleanup(); - runtimeAllocations.erase(runtime); -} - -struct ArrayBufferHolder { - explicit ArrayBufferHolder(std::shared_ptr buffer) : buffer(std::move(buffer)) {} - - std::shared_ptr buffer; - v8::Global object; -}; - -inline v8::Local makeV8String(v8::Isolate* isolate, const std::string& value) { - return v8::String::NewFromUtf8(isolate, value.c_str(), v8::NewStringType::kNormal, - static_cast(value.size())) - .ToLocalChecked(); -} - -inline std::string toUtf8(v8::Isolate* isolate, v8::Local value) { - if (value.IsEmpty()) { - return {}; - } - v8::String::Utf8Value utf8(isolate, value); - return *utf8 != nullptr ? std::string(*utf8, utf8.length()) : std::string(); -} - -inline std::string propertyNameToUtf8(v8::Isolate* isolate, v8::Local property) { - if (property->IsSymbol() && - property.As()->StrictEquals(v8::Symbol::GetIterator(isolate))) { - return "Symbol.iterator"; - } - return toUtf8(isolate, property); -} - -inline std::string currentExceptionMessage(v8::Isolate* isolate, v8::TryCatch& tryCatch) { - if (tryCatch.HasCaught()) { - return toUtf8(isolate, tryCatch.Exception()); - } - return "NativeScript V8 engine operation failed."; -} - -inline void throwV8Exception(v8::Isolate* isolate, const std::exception& exception) { - isolate->ThrowException(v8::Exception::Error(makeV8String(isolate, exception.what()))); -} - -} // namespace v8engine - -class Runtime { - public: - Runtime(v8::Isolate* isolate, v8::Local context) - : state_(std::make_shared(isolate, context)) {} - - explicit Runtime(std::shared_ptr state) : state_(std::move(state)) {} - - v8::Isolate* isolate() const { return state_->isolate; } - v8::Local context() const { return state_->localContext(); } - v8engine::RuntimeState* rawState() const { return state_.get(); } - std::shared_ptr state() const { return state_; } - const void* identity() const { return state_.get(); } - - Object global(); - - Value evaluateJavaScript(std::shared_ptr buffer, const std::string& sourceURL); - - void drainMicrotasks() { isolate()->PerformMicrotaskCheckpoint(); } - - private: - std::shared_ptr state_; -}; - -class String { - public: - String() = default; - String(Runtime& runtime, v8::Local value); - - static String createFromUtf8(Runtime& runtime, const char* value) { - return String(runtime, - v8engine::makeV8String(runtime.isolate(), value != nullptr ? value : "")); - } - - static String createFromUtf8(Runtime& runtime, const std::string& value) { - return String(runtime, v8engine::makeV8String(runtime.isolate(), value)); - } - - static String createFromUtf8(Runtime& runtime, const uint8_t* value, size_t length) { - return String(runtime, v8::String::NewFromUtf8( - runtime.isolate(), - reinterpret_cast( - value != nullptr ? value : reinterpret_cast("")), - v8::NewStringType::kNormal, static_cast(length)) - .ToLocalChecked()); - } - - std::string utf8(Runtime& runtime) const { - return v8engine::toUtf8(runtime.isolate(), local(runtime)); - } - - v8::Local local(Runtime& runtime) const { - return storage_->value.Get(runtime.isolate()).As(); - } - - operator Value() const; - - private: - friend class Value; - std::shared_ptr storage_; -}; - -class Value { - public: - Value() : kind_(v8engine::ValueStorage::Kind::Undefined) {} - - Value(bool value) : kind_(v8engine::ValueStorage::Kind::Bool), boolValue_(value) {} - - Value(double value) : kind_(v8engine::ValueStorage::Kind::Number), numberValue_(value) {} - - Value(int value) : Value(static_cast(value)) {} - Value(uint32_t value) : Value(static_cast(value)) {} - - Value(Runtime& runtime, const Value& value) { - if (value.kind_ == v8engine::ValueStorage::Kind::V8Borrowed) { - // Promote borrowed to owned - storage_ = - std::make_shared(v8engine::ValueStorage::Kind::V8); - storage_->value.Reset(runtime.isolate(), value.borrowedValue_); - kind_ = v8engine::ValueStorage::Kind::V8; - return; - } - kind_ = value.kind_; - boolValue_ = value.boolValue_; - numberValue_ = value.numberValue_; - borrowedValue_ = value.borrowedValue_; - storage_ = value.storage_; - } - Value(Runtime& runtime, Value&& value) - : kind_(value.kind_), - boolValue_(value.boolValue_), - numberValue_(value.numberValue_), - borrowedValue_(value.borrowedValue_), - storage_(std::move(value.storage_)) {} - Value(Runtime& runtime, const String& value); - Value(Runtime& runtime, const Object& object); - Value(Runtime& runtime, const Function& function); - Value(Runtime& runtime, const Array& array); - Value(Runtime& runtime, const ArrayBuffer& arrayBuffer); - Value(Runtime& runtime, const BigInt& bigint); - - static Value undefined() { return Value(); } - - static Value null() { - Value value; - value.kind_ = v8engine::ValueStorage::Kind::Null; - return value; - } - - static bool strictEquals(Runtime& runtime, const Value& lhs, - const Value& rhs) { - return lhs.local(runtime)->StrictEquals(rhs.local(runtime)); - } - - bool isUndefined() const; - bool isNull() const; - bool isBool() const; - bool getBool() const; - bool isNumber() const; - double getNumber() const; - - bool isObject() const; - bool isString() const; - bool isBigInt() const; - bool isSymbol() const; - - Object asObject(Runtime& runtime) const; - String asString(Runtime& runtime) const; - BigInt getBigInt(Runtime& runtime) const; - - v8::Local local(Runtime& runtime) const { - v8::Isolate* isolate = runtime.isolate(); - switch (kind_) { - case v8engine::ValueStorage::Kind::Undefined: - return v8::Undefined(isolate); - case v8engine::ValueStorage::Kind::Null: - return v8::Null(isolate); - case v8engine::ValueStorage::Kind::Bool: - return v8::Boolean::New(isolate, boolValue_); - case v8engine::ValueStorage::Kind::Number: - return v8::Number::New(isolate, numberValue_); - case v8engine::ValueStorage::Kind::V8: - return storage_->value.Get(isolate); - case v8engine::ValueStorage::Kind::V8Borrowed: - return borrowedValue_; - } - } - - Value(Runtime& runtime, v8::Local value) - : kind_(v8engine::ValueStorage::Kind::V8), - storage_(std::make_shared(v8engine::ValueStorage::Kind::V8)) { - storage_->value.Reset(runtime.isolate(), value); - } - - static Value borrowed(Runtime&, v8::Local value) { - Value result; - result.kind_ = v8engine::ValueStorage::Kind::V8Borrowed; - result.borrowedValue_ = value; - return result; - } - - // Access the shared storage (for Object/Function/Array interop) - std::shared_ptr storage() const { return storage_; } - - static Value fromStorage(std::shared_ptr s) { - Value v; - v.kind_ = s->kind; - v.boolValue_ = s->boolValue; - v.numberValue_ = s->numberValue; - v.borrowedValue_ = s->borrowedValue; - v.storage_ = std::move(s); - return v; - } - - private: - friend class Runtime; - friend class Object; - friend class String; - friend class BigInt; - friend class ArrayBuffer; - friend class Function; - friend class Array; - - v8engine::ValueStorage::Kind kind_ = v8engine::ValueStorage::Kind::Undefined; - bool boolValue_ = false; - double numberValue_ = 0; - v8::Local borrowedValue_; - std::shared_ptr storage_; -}; - -class Object { - public: - Object() = default; - explicit Object(Runtime& runtime) - : storage_(std::make_shared(v8engine::ValueStorage::Kind::V8)) { - storage_->value.Reset(runtime.isolate(), v8::Object::New(runtime.isolate())); - } - - static Object fromValueStorage(std::shared_ptr storage) { - Object object; - object.storage_ = std::move(storage); - return object; - } - - template - static Object createFromHostObject(Runtime& runtime, std::shared_ptr host) { - auto baseHost = std::static_pointer_cast(std::move(host)); - return createFromHostObjectWithToken(runtime, std::move(baseHost), - v8engine::hostObjectTypeToken()); - } - - // Create a native object instance using kNonMasking template for fast - // prototype-based property access. - template - static Object createNativeInstanceHostObject(Runtime& runtime, std::shared_ptr host) { - auto baseHost = std::static_pointer_cast(std::move(host)); - return createNativeInstanceWithToken(runtime, std::move(baseHost), - v8engine::hostObjectTypeToken()); - } - - static Object createNativeInstanceWithToken(Runtime& runtime, std::shared_ptr host, - const void* typeToken); - - Value getProperty(Runtime& runtime, const char* name) const { - return getProperty(runtime, - v8engine::makeV8String(runtime.isolate(), name != nullptr ? name : "")); - } - - Value getProperty(Runtime& runtime, const std::string& name) const { - return getProperty(runtime, name.c_str()); - } - - Value getProperty(Runtime& runtime, const Value& key) const { - return getProperty(runtime, key.local(runtime)); - } - - Value getProperty(Runtime& runtime, v8::Local key) const { - v8::TryCatch tryCatch(runtime.isolate()); - v8::Local result; - if (!local(runtime)->Get(runtime.context(), key).ToLocal(&result)) { - throw JSError(runtime, v8engine::currentExceptionMessage(runtime.isolate(), tryCatch)); - } - return Value(runtime, result); - } - - Object getPropertyAsObject(Runtime& runtime, const char* name) const { - return getProperty(runtime, name).asObject(runtime); - } - - Function getPropertyAsFunction(Runtime& runtime, const char* name) const; - - void setProperty(Runtime& runtime, const char* name, const Value& value) { - setProperty(runtime, v8engine::makeV8String(runtime.isolate(), name != nullptr ? name : ""), - value); - } - - void setProperty(Runtime& runtime, const char* name, const String& value) { - setProperty(runtime, name, Value(runtime, value)); - } - - void setProperty(Runtime& runtime, const char* name, const Object& value) { - setProperty(runtime, name, Value(runtime, value)); - } - - void setProperty(Runtime& runtime, const char* name, const Function& value); - void setProperty(Runtime& runtime, const char* name, const Array& value); - void setProperty(Runtime& runtime, const char* name, const ArrayBuffer& value); - void setProperty(Runtime& runtime, const char* name, bool value) { - setProperty(runtime, name, Value(value)); - } - void setProperty(Runtime& runtime, const char* name, double value) { - setProperty(runtime, name, Value(value)); - } - - void setProperty(Runtime& runtime, const std::string& name, const Value& value) { - setProperty(runtime, name.c_str(), value); - } - - void setProperty(Runtime& runtime, const Value& key, const Value& value) { - setProperty(runtime, key.local(runtime), value); - } - - void setProperty(Runtime& runtime, v8::Local key, const Value& value) { - v8::TryCatch tryCatch(runtime.isolate()); - if (!local(runtime)->Set(runtime.context(), key, value.local(runtime)).FromMaybe(false)) { - throw JSError(runtime, v8engine::currentExceptionMessage(runtime.isolate(), tryCatch)); - } - } - - bool hasProperty(Runtime& runtime, const char* name) const { - v8::TryCatch tryCatch(runtime.isolate()); - return local(runtime) - ->Has(runtime.context(), - v8engine::makeV8String(runtime.isolate(), name != nullptr ? name : "")) - .FromMaybe(false); - } - - bool isFunction(Runtime& runtime) const { return local(runtime)->IsFunction(); } - bool isArray(Runtime& runtime) const { return local(runtime)->IsArray(); } - bool isArrayBuffer(Runtime& runtime) const { return local(runtime)->IsArrayBuffer(); } - - Function asFunction(Runtime& runtime) const; - Array getArray(Runtime& runtime) const; - ArrayBuffer getArrayBuffer(Runtime& runtime) const; - Array getPropertyNames(Runtime& runtime) const; - - template - bool isHostObject(Runtime& runtime) const { - auto holder = hostObjectHolder(runtime); - return holder != nullptr && holder->typeToken == v8engine::hostObjectTypeToken(); - } - - template - std::shared_ptr getHostObject(Runtime& runtime) const { - auto holder = hostObjectHolder(runtime); - if (holder == nullptr || holder->typeToken != v8engine::hostObjectTypeToken()) { - return nullptr; - } - return std::static_pointer_cast(holder->hostObject); - } - - v8::Local local(Runtime& runtime) const { - if (storage_->kind == v8engine::ValueStorage::Kind::V8Borrowed) { - return storage_->borrowedValue.As(); - } - return storage_->value.Get(runtime.isolate()).As(); - } - - operator Value() const { - return Value::fromStorage(storage_); - } - - protected: - friend class Value; - friend class Runtime; - friend class Function; - friend class Array; - friend class ArrayBuffer; - - explicit Object(std::shared_ptr storage) : storage_(std::move(storage)) {} - - static Object createFromHostObjectWithToken(Runtime& runtime, std::shared_ptr host, - const void* typeToken); - - v8engine::HostObjectHolder* hostObjectHolder(Runtime& runtime) const { - v8::Local object = local(runtime); - if (object->InternalFieldCount() < 1) { - return nullptr; - } - return static_cast(object->GetAlignedPointerFromInternalField(0, v8::kEmbedderDataTypeTagDefault)); - } - - std::shared_ptr storage_; -}; - -class Function : public Object { - public: - Function() = default; - explicit Function(Object object) : Object(std::move(object.storage_)) {} - - static Function createFromHostFunction(Runtime& runtime, const PropNameID& name, unsigned int, - HostFunctionType callback); - - Value call(Runtime& runtime, const Value* args, size_t count) const { - v8::TryCatch tryCatch(runtime.isolate()); - std::vector> argv; - argv.reserve(count); - for (size_t i = 0; i < count; i++) { - argv.push_back(args[i].local(runtime)); - } - v8::Local result; - if (!local(runtime) - .As() - ->Call(runtime.context(), runtime.context()->Global(), static_cast(argv.size()), - argv.data()) - .ToLocal(&result)) { - throw JSError(runtime, v8engine::currentExceptionMessage(runtime.isolate(), tryCatch)); - } - return Value(runtime, result); - } - - Value call(Runtime& runtime) const { - return call(runtime, static_cast(nullptr), 0); - } - - Value call(Runtime& runtime, std::nullptr_t, size_t) const { - return call(runtime, static_cast(nullptr), 0); - } - - template - Value call(Runtime& runtime, const Value (&args)[N], size_t count) const { - return call(runtime, static_cast(args), count); - } - - template - Value call(Runtime& runtime, Args&&... args) const { - Value argv[] = {Value(runtime, std::forward(args))...}; - return call(runtime, static_cast(argv), sizeof...(Args)); - } - - Value callWithThis(Runtime& runtime, const Object& thisObject, const Value* args = nullptr, - size_t count = 0) const { - v8::TryCatch tryCatch(runtime.isolate()); - std::vector> argv; - argv.reserve(count); - for (size_t i = 0; i < count; i++) { - argv.push_back(args[i].local(runtime)); - } - v8::Local result; - if (!local(runtime) - .As() - ->Call(runtime.context(), thisObject.local(runtime), static_cast(argv.size()), - argv.data()) - .ToLocal(&result)) { - throw JSError(runtime, v8engine::currentExceptionMessage(runtime.isolate(), tryCatch)); - } - return Value(runtime, result); - } - - Value callAsConstructor(Runtime& runtime, const Value* args, size_t count) const { - v8::TryCatch tryCatch(runtime.isolate()); - std::vector> argv; - argv.reserve(count); - for (size_t i = 0; i < count; i++) { - argv.push_back(args[i].local(runtime)); - } - v8::Local result; - if (!local(runtime) - .As() - ->NewInstance(runtime.context(), static_cast(argv.size()), argv.data()) - .ToLocal(&result)) { - throw JSError(runtime, v8engine::currentExceptionMessage(runtime.isolate(), tryCatch)); - } - return Value(runtime, result); - } - - Value callAsConstructor(Runtime& runtime, std::nullptr_t, size_t) const { - return callAsConstructor(runtime, static_cast(nullptr), 0); - } - - template - Value callAsConstructor(Runtime& runtime, const Value (&args)[N], size_t count) const { - return callAsConstructor(runtime, static_cast(args), count); - } - - template - Value callAsConstructor(Runtime& runtime, Args&&... args) const { - Value argv[] = {Value(runtime, std::forward(args))...}; - return callAsConstructor(runtime, static_cast(argv), sizeof...(Args)); - } - - operator Value() const { - return Value::fromStorage(storage_); - } -}; - -class Array : public Object { - public: - explicit Array(Runtime& runtime, size_t size) - : Object(std::make_shared(v8engine::ValueStorage::Kind::V8)) { - storage_->value.Reset(runtime.isolate(), - v8::Array::New(runtime.isolate(), static_cast(size))); - } - - explicit Array(Object object) : Object(std::move(object.storage_)) {} - - size_t size(Runtime& runtime) const { return local(runtime).As()->Length(); } - - Value getValueAtIndex(Runtime& runtime, size_t index) const { - v8::TryCatch tryCatch(runtime.isolate()); - v8::Local result; - if (!local(runtime)->Get(runtime.context(), static_cast(index)).ToLocal(&result)) { - throw JSError(runtime, v8engine::currentExceptionMessage(runtime.isolate(), tryCatch)); - } - return Value(runtime, result); - } - - void setValueAtIndex(Runtime& runtime, size_t index, const Value& value) { - v8::TryCatch tryCatch(runtime.isolate()); - if (!local(runtime) - ->Set(runtime.context(), static_cast(index), value.local(runtime)) - .FromMaybe(false)) { - throw JSError(runtime, v8engine::currentExceptionMessage(runtime.isolate(), tryCatch)); - } - } - - void setValueAtIndex(Runtime& runtime, size_t index, const String& value) { - setValueAtIndex(runtime, index, Value(runtime, value)); - } - - operator Value() const { - return Value::fromStorage(storage_); - } -}; - -class BigInt { - public: - BigInt() = default; - BigInt(Runtime& runtime, v8::Local value) - : storage_(std::make_shared(v8engine::ValueStorage::Kind::V8)) { - storage_->value.Reset(runtime.isolate(), value); - } - - static BigInt fromInt64(Runtime& runtime, int64_t value) { - return BigInt(runtime, v8::BigInt::New(runtime.isolate(), value)); - } - - static BigInt fromUint64(Runtime& runtime, uint64_t value) { - return BigInt(runtime, v8::BigInt::NewFromUnsigned(runtime.isolate(), value)); - } - - String toString(Runtime& runtime, int radix) const { - v8::TryCatch tryCatch(runtime.isolate()); - v8::Local result; - (void)radix; - if (!local(runtime)->ToString(runtime.context()).ToLocal(&result)) { - throw JSError(runtime, v8engine::currentExceptionMessage(runtime.isolate(), tryCatch)); - } - return String(runtime, result); - } - - v8::Local local(Runtime& runtime) const { - return storage_->value.Get(runtime.isolate()).As(); - } - - operator Value() const { - return Value::fromStorage(storage_); - } - - private: - friend class Value; - std::shared_ptr storage_; -}; - -class ArrayBuffer : public Object { - public: - ArrayBuffer(Runtime& runtime, std::shared_ptr buffer) - : Object(std::make_shared(v8engine::ValueStorage::Kind::V8)) { - auto holder = new v8engine::ArrayBufferHolder(std::move(buffer)); - auto backingStore = v8::ArrayBuffer::NewBackingStore( - holder->buffer->data(), holder->buffer->size(), - [](void*, size_t, void* deleterData) { - auto* holder = static_cast(deleterData); - holder->object.Reset(); - delete holder; - }, - holder); - v8::Local arrayBuffer = - v8::ArrayBuffer::New(runtime.isolate(), std::move(backingStore)); - storage_->value.Reset(runtime.isolate(), arrayBuffer); - holder->object.Reset(runtime.isolate(), arrayBuffer); - } - - explicit ArrayBuffer(Object object) : Object(std::move(object.storage_)) {} - - size_t size(Runtime& runtime) const { return local(runtime).As()->ByteLength(); } - - uint8_t* data(Runtime& runtime) const { - auto backingStore = local(runtime).As()->GetBackingStore(); - return static_cast(backingStore->Data()); - } - - operator Value() const { - return Value::fromStorage(storage_); - } -}; -} // namespace engine -} // namespace nativescript - #endif // TARGET_ENGINE_V8 #endif // NATIVESCRIPT_FFI_V8_NATIVE_API_V8_RUNTIME_H diff --git a/NativeScript/ffi/objc/v8/NativeApiV8HostObjects.mm b/NativeScript/jsi/v8/V8HostObjects.cpp similarity index 99% rename from NativeScript/ffi/objc/v8/NativeApiV8HostObjects.mm rename to NativeScript/jsi/v8/V8HostObjects.cpp index cd26fca47..eb06bcab5 100644 --- a/NativeScript/ffi/objc/v8/NativeApiV8HostObjects.mm +++ b/NativeScript/jsi/v8/V8HostObjects.cpp @@ -1,4 +1,4 @@ -#include "NativeApiV8Runtime.h" +#include "jsi/v8/V8Runtime.h" #include "jsi/shared/NativeApiStackValueArray.h" #ifdef TARGET_ENGINE_V8 diff --git a/NativeScript/ffi/objc/v8/NativeApiV8Runtime.mm b/NativeScript/jsi/v8/V8Runtime.cpp similarity index 97% rename from NativeScript/ffi/objc/v8/NativeApiV8Runtime.mm rename to NativeScript/jsi/v8/V8Runtime.cpp index 10f8e3d86..0be822dfd 100644 --- a/NativeScript/ffi/objc/v8/NativeApiV8Runtime.mm +++ b/NativeScript/jsi/v8/V8Runtime.cpp @@ -1,4 +1,4 @@ -#include "NativeApiV8Runtime.h" +#include "jsi/v8/V8Runtime.h" #ifdef TARGET_ENGINE_V8 diff --git a/NativeScript/jsi/v8/V8Runtime.h b/NativeScript/jsi/v8/V8Runtime.h new file mode 100644 index 000000000..bb55fcef3 --- /dev/null +++ b/NativeScript/jsi/v8/V8Runtime.h @@ -0,0 +1,851 @@ +#ifndef NATIVESCRIPT_JSI_V8_V8_RUNTIME_H +#define NATIVESCRIPT_JSI_V8_V8_RUNTIME_H + +// V8 behind the JSI-shaped `nativescript::engine` API. +// +// This header is platform-neutral: it depends on V8 and the C++ standard +// library, and on nothing from Foundation, the Objective-C runtime, or the +// Apple metadata format. Android compiles it as-is. +// +// The Apple bridge's extra baggage lives in ffi/objc/v8/NativeApiV8Runtime.h, +// which includes this file. Apple sources keep including that path unchanged. + +#ifdef TARGET_ENGINE_V8 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "jsi/shared/RuntimeCleanupRegistry.h" +#include "v8.h" + +namespace nativescript { +namespace engine { + +class Runtime; +class Value; +class Object; +class Function; +class Array; +class String; +class BigInt; +class ArrayBuffer; + +class JSError : public std::runtime_error { + public: + JSError(Runtime&, const std::string& message) : std::runtime_error(message) {} + explicit JSError(const std::string& message) : std::runtime_error(message) {} +}; + +class StringBuffer { + public: + explicit StringBuffer(std::string value) : value_(std::move(value)) {} + const char* data() const { return value_.data(); } + size_t size() const { return value_.size(); } + + private: + std::string value_; +}; + +class MutableBuffer { + public: + virtual ~MutableBuffer() = default; + virtual size_t size() const = 0; + virtual uint8_t* data() = 0; +}; + +class PropNameID { + public: + PropNameID() = default; + explicit PropNameID(std::string value) : value_(std::move(value)) {} + + static PropNameID forAscii(Runtime&, const char* value) { + return PropNameID(value != nullptr ? value : ""); + } + + static PropNameID forAscii(Runtime&, const std::string& value) { return PropNameID(value); } + + std::string utf8(Runtime&) const { return value_; } + + private: + std::string value_; +}; + +class HostObject { + public: + virtual ~HostObject() = default; + virtual Value get(Runtime& runtime, const PropNameID& name); + virtual bool set(Runtime& runtime, const PropNameID& name, const Value& value); + virtual std::vector getPropertyNames(Runtime& runtime); +}; + +using HostFunctionType = std::function; + +namespace v8engine { + +struct RuntimeState { + explicit RuntimeState(v8::Isolate* isolate, v8::Local context) : isolate(isolate) { + this->context.Reset(isolate, context); + } + + ~RuntimeState() { + nativeClassArgumentLast.value.Reset(); + nativeClassArgumentLast.nativeClass = nullptr; + for (auto& entry : nativeClassArgumentCache) { + entry.value.Reset(); + entry.nativeClass = nullptr; + } + nativeSelectorArgumentLast.value.Reset(); + nativeSelectorArgumentLast.selector = nullptr; + for (auto& entry : nativeSelectorArgumentCache) { + entry.value.Reset(); + entry.selector = nullptr; + } + context.Reset(); + } + + v8::Local localContext() const { + v8::Local ctx = context.Get(isolate); + return ctx.IsEmpty() ? isolate->GetCurrentContext() : ctx; + } + + v8::Isolate* isolate = nullptr; + v8::Global context; + v8::Global hostObjectTemplate; + v8::Global nativeObjectTemplate; // kNonMasking for instances + std::vector> retainedNativeData; + // Argument-marshalling caches: they memoise "this JS value denotes that + // native type / method" so a repeated call does not re-resolve it. + // + // Both native handles are held opaquely so this struct stays platform + // neutral. They are Class and SEL on Apple, and the JNI equivalents + // (jclass, jmethodID) on Android -- all four are pointers. The bridge that + // populates and reads these casts back to its own types; see + // ffi/objc/v8/NativeApiV8Marshalling.mm for the Apple side. + struct NativeClassArgumentCacheEntry { + v8::Global value; + const void* nativeClass = nullptr; + }; + NativeClassArgumentCacheEntry nativeClassArgumentLast; + NativeClassArgumentCacheEntry nativeClassArgumentCache[4]; + size_t nativeClassArgumentCacheNext = 0; + struct NativeSelectorArgumentCacheEntry { + v8::Global value; + const void* selector = nullptr; + }; + NativeSelectorArgumentCacheEntry nativeSelectorArgumentLast; + NativeSelectorArgumentCacheEntry nativeSelectorArgumentCache[4]; + size_t nativeSelectorArgumentCacheNext = 0; +}; + +struct ValueStorage { + enum class Kind : uint8_t { + Undefined, + Null, + Bool, + Number, + V8, + V8Borrowed, + }; + + explicit ValueStorage(Kind kind) : kind(kind) {} + + ~ValueStorage() { value.Reset(); } + + Kind kind = Kind::Undefined; + bool boolValue = false; + double numberValue = 0; + v8::Global value; + v8::Local borrowedValue; +}; + +template +const void* hostObjectTypeToken() { + static int token = 0; + return &token; +} + +struct HostObjectHolder { + HostObjectHolder(std::shared_ptr state, std::shared_ptr hostObject, + const void* typeToken) + : state(std::move(state)), hostObject(std::move(hostObject)), typeToken(typeToken) {} + + ~HostObjectHolder() { object.Reset(); } + + std::shared_ptr state; + std::shared_ptr hostObject; + const void* typeToken = nullptr; + v8::Global object; +}; + +struct FunctionHolder { + FunctionHolder(std::shared_ptr state, HostFunctionType callback) + : state(std::move(state)), callback(std::move(callback)) {} + + ~FunctionHolder() { function.Reset(); } + + std::shared_ptr state; + HostFunctionType callback; + v8::Global function; +}; + +inline thread_local std::unordered_map + runtimeAllocations; + +template +void trackRuntimeAllocation(v8::Isolate* isolate, T* allocation) { + runtimeAllocations[isolate].track(allocation); +} + +inline void untrackRuntimeAllocation(v8::Isolate* isolate, + void* allocation) { + auto runtime = runtimeAllocations.find(isolate); + if (runtime == runtimeAllocations.end()) { + return; + } + runtime->second.untrack(allocation); + if (runtime->second.empty()) { + runtimeAllocations.erase(runtime); + } +} + +inline void cleanupRuntimeAllocations(v8::Isolate* isolate) { + auto runtime = runtimeAllocations.find(isolate); + if (runtime == runtimeAllocations.end()) { + return; + } + + runtime->second.cleanup(); + runtimeAllocations.erase(runtime); +} + +struct ArrayBufferHolder { + explicit ArrayBufferHolder(std::shared_ptr buffer) : buffer(std::move(buffer)) {} + + std::shared_ptr buffer; + v8::Global object; +}; + +inline v8::Local makeV8String(v8::Isolate* isolate, const std::string& value) { + return v8::String::NewFromUtf8(isolate, value.c_str(), v8::NewStringType::kNormal, + static_cast(value.size())) + .ToLocalChecked(); +} + +inline std::string toUtf8(v8::Isolate* isolate, v8::Local value) { + if (value.IsEmpty()) { + return {}; + } + v8::String::Utf8Value utf8(isolate, value); + return *utf8 != nullptr ? std::string(*utf8, utf8.length()) : std::string(); +} + +inline std::string propertyNameToUtf8(v8::Isolate* isolate, v8::Local property) { + if (property->IsSymbol() && + property.As()->StrictEquals(v8::Symbol::GetIterator(isolate))) { + return "Symbol.iterator"; + } + return toUtf8(isolate, property); +} + +inline std::string currentExceptionMessage(v8::Isolate* isolate, v8::TryCatch& tryCatch) { + if (tryCatch.HasCaught()) { + return toUtf8(isolate, tryCatch.Exception()); + } + return "NativeScript V8 engine operation failed."; +} + +inline void throwV8Exception(v8::Isolate* isolate, const std::exception& exception) { + isolate->ThrowException(v8::Exception::Error(makeV8String(isolate, exception.what()))); +} + +} // namespace v8engine + +class Runtime { + public: + Runtime(v8::Isolate* isolate, v8::Local context) + : state_(std::make_shared(isolate, context)) {} + + explicit Runtime(std::shared_ptr state) : state_(std::move(state)) {} + + v8::Isolate* isolate() const { return state_->isolate; } + v8::Local context() const { return state_->localContext(); } + v8engine::RuntimeState* rawState() const { return state_.get(); } + std::shared_ptr state() const { return state_; } + const void* identity() const { return state_.get(); } + + Object global(); + + Value evaluateJavaScript(std::shared_ptr buffer, const std::string& sourceURL); + + void drainMicrotasks() { isolate()->PerformMicrotaskCheckpoint(); } + + private: + std::shared_ptr state_; +}; + +class String { + public: + String() = default; + String(Runtime& runtime, v8::Local value); + + static String createFromUtf8(Runtime& runtime, const char* value) { + return String(runtime, + v8engine::makeV8String(runtime.isolate(), value != nullptr ? value : "")); + } + + static String createFromUtf8(Runtime& runtime, const std::string& value) { + return String(runtime, v8engine::makeV8String(runtime.isolate(), value)); + } + + static String createFromUtf8(Runtime& runtime, const uint8_t* value, size_t length) { + return String(runtime, v8::String::NewFromUtf8( + runtime.isolate(), + reinterpret_cast( + value != nullptr ? value : reinterpret_cast("")), + v8::NewStringType::kNormal, static_cast(length)) + .ToLocalChecked()); + } + + std::string utf8(Runtime& runtime) const { + return v8engine::toUtf8(runtime.isolate(), local(runtime)); + } + + v8::Local local(Runtime& runtime) const { + return storage_->value.Get(runtime.isolate()).As(); + } + + operator Value() const; + + private: + friend class Value; + std::shared_ptr storage_; +}; + +class Value { + public: + Value() : kind_(v8engine::ValueStorage::Kind::Undefined) {} + + Value(bool value) : kind_(v8engine::ValueStorage::Kind::Bool), boolValue_(value) {} + + Value(double value) : kind_(v8engine::ValueStorage::Kind::Number), numberValue_(value) {} + + Value(int value) : Value(static_cast(value)) {} + Value(uint32_t value) : Value(static_cast(value)) {} + + Value(Runtime& runtime, const Value& value) { + if (value.kind_ == v8engine::ValueStorage::Kind::V8Borrowed) { + // Promote borrowed to owned + storage_ = + std::make_shared(v8engine::ValueStorage::Kind::V8); + storage_->value.Reset(runtime.isolate(), value.borrowedValue_); + kind_ = v8engine::ValueStorage::Kind::V8; + return; + } + kind_ = value.kind_; + boolValue_ = value.boolValue_; + numberValue_ = value.numberValue_; + borrowedValue_ = value.borrowedValue_; + storage_ = value.storage_; + } + Value(Runtime& runtime, Value&& value) + : kind_(value.kind_), + boolValue_(value.boolValue_), + numberValue_(value.numberValue_), + borrowedValue_(value.borrowedValue_), + storage_(std::move(value.storage_)) {} + Value(Runtime& runtime, const String& value); + Value(Runtime& runtime, const Object& object); + Value(Runtime& runtime, const Function& function); + Value(Runtime& runtime, const Array& array); + Value(Runtime& runtime, const ArrayBuffer& arrayBuffer); + Value(Runtime& runtime, const BigInt& bigint); + + static Value undefined() { return Value(); } + + static Value null() { + Value value; + value.kind_ = v8engine::ValueStorage::Kind::Null; + return value; + } + + static bool strictEquals(Runtime& runtime, const Value& lhs, + const Value& rhs) { + return lhs.local(runtime)->StrictEquals(rhs.local(runtime)); + } + + bool isUndefined() const; + bool isNull() const; + bool isBool() const; + bool getBool() const; + bool isNumber() const; + double getNumber() const; + + bool isObject() const; + bool isString() const; + bool isBigInt() const; + bool isSymbol() const; + + Object asObject(Runtime& runtime) const; + String asString(Runtime& runtime) const; + BigInt getBigInt(Runtime& runtime) const; + + v8::Local local(Runtime& runtime) const { + v8::Isolate* isolate = runtime.isolate(); + switch (kind_) { + case v8engine::ValueStorage::Kind::Undefined: + return v8::Undefined(isolate); + case v8engine::ValueStorage::Kind::Null: + return v8::Null(isolate); + case v8engine::ValueStorage::Kind::Bool: + return v8::Boolean::New(isolate, boolValue_); + case v8engine::ValueStorage::Kind::Number: + return v8::Number::New(isolate, numberValue_); + case v8engine::ValueStorage::Kind::V8: + return storage_->value.Get(isolate); + case v8engine::ValueStorage::Kind::V8Borrowed: + return borrowedValue_; + } + } + + Value(Runtime& runtime, v8::Local value) + : kind_(v8engine::ValueStorage::Kind::V8), + storage_(std::make_shared(v8engine::ValueStorage::Kind::V8)) { + storage_->value.Reset(runtime.isolate(), value); + } + + static Value borrowed(Runtime&, v8::Local value) { + Value result; + result.kind_ = v8engine::ValueStorage::Kind::V8Borrowed; + result.borrowedValue_ = value; + return result; + } + + // Access the shared storage (for Object/Function/Array interop) + std::shared_ptr storage() const { return storage_; } + + static Value fromStorage(std::shared_ptr s) { + Value v; + v.kind_ = s->kind; + v.boolValue_ = s->boolValue; + v.numberValue_ = s->numberValue; + v.borrowedValue_ = s->borrowedValue; + v.storage_ = std::move(s); + return v; + } + + private: + friend class Runtime; + friend class Object; + friend class String; + friend class BigInt; + friend class ArrayBuffer; + friend class Function; + friend class Array; + + v8engine::ValueStorage::Kind kind_ = v8engine::ValueStorage::Kind::Undefined; + bool boolValue_ = false; + double numberValue_ = 0; + v8::Local borrowedValue_; + std::shared_ptr storage_; +}; + +class Object { + public: + Object() = default; + explicit Object(Runtime& runtime) + : storage_(std::make_shared(v8engine::ValueStorage::Kind::V8)) { + storage_->value.Reset(runtime.isolate(), v8::Object::New(runtime.isolate())); + } + + static Object fromValueStorage(std::shared_ptr storage) { + Object object; + object.storage_ = std::move(storage); + return object; + } + + template + static Object createFromHostObject(Runtime& runtime, std::shared_ptr host) { + auto baseHost = std::static_pointer_cast(std::move(host)); + return createFromHostObjectWithToken(runtime, std::move(baseHost), + v8engine::hostObjectTypeToken()); + } + + // Create a native object instance using kNonMasking template for fast + // prototype-based property access. + template + static Object createNativeInstanceHostObject(Runtime& runtime, std::shared_ptr host) { + auto baseHost = std::static_pointer_cast(std::move(host)); + return createNativeInstanceWithToken(runtime, std::move(baseHost), + v8engine::hostObjectTypeToken()); + } + + static Object createNativeInstanceWithToken(Runtime& runtime, std::shared_ptr host, + const void* typeToken); + + Value getProperty(Runtime& runtime, const char* name) const { + return getProperty(runtime, + v8engine::makeV8String(runtime.isolate(), name != nullptr ? name : "")); + } + + Value getProperty(Runtime& runtime, const std::string& name) const { + return getProperty(runtime, name.c_str()); + } + + Value getProperty(Runtime& runtime, const Value& key) const { + return getProperty(runtime, key.local(runtime)); + } + + Value getProperty(Runtime& runtime, v8::Local key) const { + v8::TryCatch tryCatch(runtime.isolate()); + v8::Local result; + if (!local(runtime)->Get(runtime.context(), key).ToLocal(&result)) { + throw JSError(runtime, v8engine::currentExceptionMessage(runtime.isolate(), tryCatch)); + } + return Value(runtime, result); + } + + Object getPropertyAsObject(Runtime& runtime, const char* name) const { + return getProperty(runtime, name).asObject(runtime); + } + + Function getPropertyAsFunction(Runtime& runtime, const char* name) const; + + void setProperty(Runtime& runtime, const char* name, const Value& value) { + setProperty(runtime, v8engine::makeV8String(runtime.isolate(), name != nullptr ? name : ""), + value); + } + + void setProperty(Runtime& runtime, const char* name, const String& value) { + setProperty(runtime, name, Value(runtime, value)); + } + + void setProperty(Runtime& runtime, const char* name, const Object& value) { + setProperty(runtime, name, Value(runtime, value)); + } + + void setProperty(Runtime& runtime, const char* name, const Function& value); + void setProperty(Runtime& runtime, const char* name, const Array& value); + void setProperty(Runtime& runtime, const char* name, const ArrayBuffer& value); + void setProperty(Runtime& runtime, const char* name, bool value) { + setProperty(runtime, name, Value(value)); + } + void setProperty(Runtime& runtime, const char* name, double value) { + setProperty(runtime, name, Value(value)); + } + + void setProperty(Runtime& runtime, const std::string& name, const Value& value) { + setProperty(runtime, name.c_str(), value); + } + + void setProperty(Runtime& runtime, const Value& key, const Value& value) { + setProperty(runtime, key.local(runtime), value); + } + + void setProperty(Runtime& runtime, v8::Local key, const Value& value) { + v8::TryCatch tryCatch(runtime.isolate()); + if (!local(runtime)->Set(runtime.context(), key, value.local(runtime)).FromMaybe(false)) { + throw JSError(runtime, v8engine::currentExceptionMessage(runtime.isolate(), tryCatch)); + } + } + + bool hasProperty(Runtime& runtime, const char* name) const { + v8::TryCatch tryCatch(runtime.isolate()); + return local(runtime) + ->Has(runtime.context(), + v8engine::makeV8String(runtime.isolate(), name != nullptr ? name : "")) + .FromMaybe(false); + } + + bool isFunction(Runtime& runtime) const { return local(runtime)->IsFunction(); } + bool isArray(Runtime& runtime) const { return local(runtime)->IsArray(); } + bool isArrayBuffer(Runtime& runtime) const { return local(runtime)->IsArrayBuffer(); } + + Function asFunction(Runtime& runtime) const; + Array getArray(Runtime& runtime) const; + ArrayBuffer getArrayBuffer(Runtime& runtime) const; + Array getPropertyNames(Runtime& runtime) const; + + template + bool isHostObject(Runtime& runtime) const { + auto holder = hostObjectHolder(runtime); + return holder != nullptr && holder->typeToken == v8engine::hostObjectTypeToken(); + } + + template + std::shared_ptr getHostObject(Runtime& runtime) const { + auto holder = hostObjectHolder(runtime); + if (holder == nullptr || holder->typeToken != v8engine::hostObjectTypeToken()) { + return nullptr; + } + return std::static_pointer_cast(holder->hostObject); + } + + v8::Local local(Runtime& runtime) const { + if (storage_->kind == v8engine::ValueStorage::Kind::V8Borrowed) { + return storage_->borrowedValue.As(); + } + return storage_->value.Get(runtime.isolate()).As(); + } + + operator Value() const { + return Value::fromStorage(storage_); + } + + protected: + friend class Value; + friend class Runtime; + friend class Function; + friend class Array; + friend class ArrayBuffer; + + explicit Object(std::shared_ptr storage) : storage_(std::move(storage)) {} + + static Object createFromHostObjectWithToken(Runtime& runtime, std::shared_ptr host, + const void* typeToken); + + v8engine::HostObjectHolder* hostObjectHolder(Runtime& runtime) const { + v8::Local object = local(runtime); + if (object->InternalFieldCount() < 1) { + return nullptr; + } + return static_cast(object->GetAlignedPointerFromInternalField(0, v8::kEmbedderDataTypeTagDefault)); + } + + std::shared_ptr storage_; +}; + +class Function : public Object { + public: + Function() = default; + explicit Function(Object object) : Object(std::move(object.storage_)) {} + + static Function createFromHostFunction(Runtime& runtime, const PropNameID& name, unsigned int, + HostFunctionType callback); + + Value call(Runtime& runtime, const Value* args, size_t count) const { + v8::TryCatch tryCatch(runtime.isolate()); + std::vector> argv; + argv.reserve(count); + for (size_t i = 0; i < count; i++) { + argv.push_back(args[i].local(runtime)); + } + v8::Local result; + if (!local(runtime) + .As() + ->Call(runtime.context(), runtime.context()->Global(), static_cast(argv.size()), + argv.data()) + .ToLocal(&result)) { + throw JSError(runtime, v8engine::currentExceptionMessage(runtime.isolate(), tryCatch)); + } + return Value(runtime, result); + } + + Value call(Runtime& runtime) const { + return call(runtime, static_cast(nullptr), 0); + } + + Value call(Runtime& runtime, std::nullptr_t, size_t) const { + return call(runtime, static_cast(nullptr), 0); + } + + template + Value call(Runtime& runtime, const Value (&args)[N], size_t count) const { + return call(runtime, static_cast(args), count); + } + + template + Value call(Runtime& runtime, Args&&... args) const { + Value argv[] = {Value(runtime, std::forward(args))...}; + return call(runtime, static_cast(argv), sizeof...(Args)); + } + + Value callWithThis(Runtime& runtime, const Object& thisObject, const Value* args = nullptr, + size_t count = 0) const { + v8::TryCatch tryCatch(runtime.isolate()); + std::vector> argv; + argv.reserve(count); + for (size_t i = 0; i < count; i++) { + argv.push_back(args[i].local(runtime)); + } + v8::Local result; + if (!local(runtime) + .As() + ->Call(runtime.context(), thisObject.local(runtime), static_cast(argv.size()), + argv.data()) + .ToLocal(&result)) { + throw JSError(runtime, v8engine::currentExceptionMessage(runtime.isolate(), tryCatch)); + } + return Value(runtime, result); + } + + Value callAsConstructor(Runtime& runtime, const Value* args, size_t count) const { + v8::TryCatch tryCatch(runtime.isolate()); + std::vector> argv; + argv.reserve(count); + for (size_t i = 0; i < count; i++) { + argv.push_back(args[i].local(runtime)); + } + v8::Local result; + if (!local(runtime) + .As() + ->NewInstance(runtime.context(), static_cast(argv.size()), argv.data()) + .ToLocal(&result)) { + throw JSError(runtime, v8engine::currentExceptionMessage(runtime.isolate(), tryCatch)); + } + return Value(runtime, result); + } + + Value callAsConstructor(Runtime& runtime, std::nullptr_t, size_t) const { + return callAsConstructor(runtime, static_cast(nullptr), 0); + } + + template + Value callAsConstructor(Runtime& runtime, const Value (&args)[N], size_t count) const { + return callAsConstructor(runtime, static_cast(args), count); + } + + template + Value callAsConstructor(Runtime& runtime, Args&&... args) const { + Value argv[] = {Value(runtime, std::forward(args))...}; + return callAsConstructor(runtime, static_cast(argv), sizeof...(Args)); + } + + operator Value() const { + return Value::fromStorage(storage_); + } +}; + +class Array : public Object { + public: + explicit Array(Runtime& runtime, size_t size) + : Object(std::make_shared(v8engine::ValueStorage::Kind::V8)) { + storage_->value.Reset(runtime.isolate(), + v8::Array::New(runtime.isolate(), static_cast(size))); + } + + explicit Array(Object object) : Object(std::move(object.storage_)) {} + + size_t size(Runtime& runtime) const { return local(runtime).As()->Length(); } + + Value getValueAtIndex(Runtime& runtime, size_t index) const { + v8::TryCatch tryCatch(runtime.isolate()); + v8::Local result; + if (!local(runtime)->Get(runtime.context(), static_cast(index)).ToLocal(&result)) { + throw JSError(runtime, v8engine::currentExceptionMessage(runtime.isolate(), tryCatch)); + } + return Value(runtime, result); + } + + void setValueAtIndex(Runtime& runtime, size_t index, const Value& value) { + v8::TryCatch tryCatch(runtime.isolate()); + if (!local(runtime) + ->Set(runtime.context(), static_cast(index), value.local(runtime)) + .FromMaybe(false)) { + throw JSError(runtime, v8engine::currentExceptionMessage(runtime.isolate(), tryCatch)); + } + } + + void setValueAtIndex(Runtime& runtime, size_t index, const String& value) { + setValueAtIndex(runtime, index, Value(runtime, value)); + } + + operator Value() const { + return Value::fromStorage(storage_); + } +}; + +class BigInt { + public: + BigInt() = default; + BigInt(Runtime& runtime, v8::Local value) + : storage_(std::make_shared(v8engine::ValueStorage::Kind::V8)) { + storage_->value.Reset(runtime.isolate(), value); + } + + static BigInt fromInt64(Runtime& runtime, int64_t value) { + return BigInt(runtime, v8::BigInt::New(runtime.isolate(), value)); + } + + static BigInt fromUint64(Runtime& runtime, uint64_t value) { + return BigInt(runtime, v8::BigInt::NewFromUnsigned(runtime.isolate(), value)); + } + + String toString(Runtime& runtime, int radix) const { + v8::TryCatch tryCatch(runtime.isolate()); + v8::Local result; + (void)radix; + if (!local(runtime)->ToString(runtime.context()).ToLocal(&result)) { + throw JSError(runtime, v8engine::currentExceptionMessage(runtime.isolate(), tryCatch)); + } + return String(runtime, result); + } + + v8::Local local(Runtime& runtime) const { + return storage_->value.Get(runtime.isolate()).As(); + } + + operator Value() const { + return Value::fromStorage(storage_); + } + + private: + friend class Value; + std::shared_ptr storage_; +}; + +class ArrayBuffer : public Object { + public: + ArrayBuffer(Runtime& runtime, std::shared_ptr buffer) + : Object(std::make_shared(v8engine::ValueStorage::Kind::V8)) { + auto holder = new v8engine::ArrayBufferHolder(std::move(buffer)); + auto backingStore = v8::ArrayBuffer::NewBackingStore( + holder->buffer->data(), holder->buffer->size(), + [](void*, size_t, void* deleterData) { + auto* holder = static_cast(deleterData); + holder->object.Reset(); + delete holder; + }, + holder); + v8::Local arrayBuffer = + v8::ArrayBuffer::New(runtime.isolate(), std::move(backingStore)); + storage_->value.Reset(runtime.isolate(), arrayBuffer); + holder->object.Reset(runtime.isolate(), arrayBuffer); + } + + explicit ArrayBuffer(Object object) : Object(std::move(object.storage_)) {} + + size_t size(Runtime& runtime) const { return local(runtime).As()->ByteLength(); } + + uint8_t* data(Runtime& runtime) const { + auto backingStore = local(runtime).As()->GetBackingStore(); + return static_cast(backingStore->Data()); + } + + operator Value() const { + return Value::fromStorage(storage_); + } +}; +} // namespace engine +} // namespace nativescript + +#endif // TARGET_ENGINE_V8 + +#endif // NATIVESCRIPT_JSI_V8_V8_RUNTIME_H diff --git a/NativeScript/ffi/objc/v8/NativeApiV8Value.mm b/NativeScript/jsi/v8/V8Value.cpp similarity index 99% rename from NativeScript/ffi/objc/v8/NativeApiV8Value.mm rename to NativeScript/jsi/v8/V8Value.cpp index dab1e0271..5ac94b6af 100644 --- a/NativeScript/ffi/objc/v8/NativeApiV8Value.mm +++ b/NativeScript/jsi/v8/V8Value.cpp @@ -1,4 +1,4 @@ -#include "NativeApiV8Runtime.h" +#include "jsi/v8/V8Runtime.h" #ifdef TARGET_ENGINE_V8 From b4c1c4aec32918b184e8f6707ed2bac6fc8aad3b Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Thu, 30 Jul 2026 23:17:33 +0500 Subject: [PATCH 4/7] refactor(jsi): move the JavaScriptCore engine layer into the shared tree Same split as the other two engines: engine layer to jsi/jsc/ as .cpp, Apple preamble left in a forwarding header at the original path. No Apple include changed. JSC's one genuine tie to Foundation was makeJSString, which built a JSString by routing the UTF-8 through NSString to get UTF-16. Replaced with a strict UTF-8 decoder in plain C++. JSStringCreateWithUTF8CString is not a substitute on its own, which is presumably why the NSString path existed: it takes a NUL-terminated C string, so it silently truncates at an embedded U+0000 -- a legal JavaScript character. The decoder preserves the old behaviour exactly, including falling back to JSStringCreateWithUTF8CString on malformed input, which is what the previous code did when NSString returned nil. The decoder was unit-tested standalone before being wired in: 16 cases covering ASCII, embedded NUL, 2/3/4-byte sequences, surrogate-pair splitting, U+10FFFF, and the rejection paths (lone continuation, truncation, overlong forms, directly-encoded surrogates, out-of-range, invalid lead bytes). Verified: iOS TestRunner on JSC, 713 specs, 1 failure -- byte-identical to the pre-move run, same spec (ApiTests SpecialCaseProperty_When_CustomSelector_ ImplementedInJS), which fails on this branch's parent too. (cherry picked from commit 8a1221e6cab46319bd1ea73f4aa7b7e70d8542ed) --- NativeScript/CMakeLists.txt | 8 +- .../ffi/objc/jsc/NativeApiJSCRuntime.h | 895 +--------------- .../jsc/JSCHostObjects.cpp} | 2 +- .../jsc/JSCRuntime.cpp} | 2 +- NativeScript/jsi/jsc/JSCRuntime.h | 963 ++++++++++++++++++ .../jsc/JSCValue.cpp} | 2 +- 6 files changed, 983 insertions(+), 889 deletions(-) rename NativeScript/{ffi/objc/jsc/NativeApiJSCHostObjects.mm => jsi/jsc/JSCHostObjects.cpp} (99%) rename NativeScript/{ffi/objc/jsc/NativeApiJSCRuntime.mm => jsi/jsc/JSCRuntime.cpp} (96%) create mode 100644 NativeScript/jsi/jsc/JSCRuntime.h rename NativeScript/{ffi/objc/jsc/NativeApiJSCValue.mm => jsi/jsc/JSCValue.cpp} (99%) diff --git a/NativeScript/CMakeLists.txt b/NativeScript/CMakeLists.txt index 99078b8bd..d72159f7a 100644 --- a/NativeScript/CMakeLists.txt +++ b/NativeScript/CMakeLists.txt @@ -284,10 +284,12 @@ set(FFI_V8_ENGINE_SOURCE_FILES set(FFI_JSC_ENGINE_SOURCE_FILES ${FFI_ENGINE_SHARED_SOURCE_FILES} + # Platform-neutral engine layer, shared with the Android runtime. + jsi/jsc/JSCHostObjects.cpp + jsi/jsc/JSCRuntime.cpp + jsi/jsc/JSCValue.cpp + # Apple-only ObjC bridge on top of it. ffi/objc/jsc/NativeApiJSC.mm - ffi/objc/jsc/NativeApiJSCHostObjects.mm - ffi/objc/jsc/NativeApiJSCRuntime.mm - ffi/objc/jsc/NativeApiJSCValue.mm ) set(FFI_QUICKJS_ENGINE_SOURCE_FILES diff --git a/NativeScript/ffi/objc/jsc/NativeApiJSCRuntime.h b/NativeScript/ffi/objc/jsc/NativeApiJSCRuntime.h index d01c47039..4c595b1c5 100644 --- a/NativeScript/ffi/objc/jsc/NativeApiJSCRuntime.h +++ b/NativeScript/ffi/objc/jsc/NativeApiJSCRuntime.h @@ -1,11 +1,19 @@ #ifndef NATIVESCRIPT_FFI_JSC_NATIVE_API_JSC_RUNTIME_H #define NATIVESCRIPT_FFI_JSC_NATIVE_API_JSC_RUNTIME_H +// Apple-side entry point for the JavaScriptCore engine layer. +// +// The engine layer itself -- the `nativescript::engine` classes wrapping JSC +// -- moved to jsi/jsc/JSCRuntime.h so the Android runtime can compile it too. +// What stays here is exactly what the Apple ObjC bridge needs on top of it and +// Android has no use for: Foundation, the Objective-C runtime, the Mach-O +// metadata section lookup, and the metadata reader. +// +// Apple sources include this path, unchanged, and get both halves. + #ifdef TARGET_ENGINE_JSC #import -#include -#include #include #include #include @@ -13,32 +21,12 @@ #include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "../shared/RuntimeCleanupRegistry.h" - #include "Metadata.h" #include "MetadataReader.h" #include "ffi.h" +#include "jsi/jsc/JSCRuntime.h" + @protocol NativeApiClassBuilderProtocol @end @@ -46,865 +34,6 @@ extern const unsigned char embedded_metadata[EMBED_METADATA_SIZE]; #endif -namespace nativescript { -namespace engine { - -class Runtime; -class Value; -class Object; -class Function; -class Array; -class String; -class BigInt; -class ArrayBuffer; - -class JSError : public std::runtime_error { - public: - JSError(Runtime&, const std::string& message) : std::runtime_error(message) {} - explicit JSError(const std::string& message) : std::runtime_error(message) {} -}; - -class StringBuffer { - public: - explicit StringBuffer(std::string value) : value_(std::move(value)) {} - const char* data() const { return value_.data(); } - size_t size() const { return value_.size(); } - - private: - std::string value_; -}; - -class MutableBuffer { - public: - virtual ~MutableBuffer() = default; - virtual size_t size() const = 0; - virtual uint8_t* data() = 0; -}; - -class PropNameID { - public: - PropNameID() = default; - explicit PropNameID(std::string value) : value_(std::move(value)) {} - - static PropNameID forAscii(Runtime&, const char* value) { - return PropNameID(value != nullptr ? value : ""); - } - - static PropNameID forAscii(Runtime&, const std::string& value) { return PropNameID(value); } - - std::string utf8(Runtime&) const { return value_; } - - private: - std::string value_; -}; - -class HostObject { - public: - virtual ~HostObject() = default; - virtual Value get(Runtime& runtime, const PropNameID& name); - virtual bool set(Runtime& runtime, const PropNameID& name, const Value& value); - virtual std::vector getPropertyNames(Runtime& runtime); -}; - -using HostFunctionType = std::function; - -namespace jscengine { - -inline std::string stringToUtf8(JSStringRef string) { - if (string == nullptr) { - return {}; - } - size_t capacity = JSStringGetMaximumUTF8CStringSize(string); - std::string result(capacity, '\0'); - size_t written = JSStringGetUTF8CString(string, result.data(), capacity); - if (written == 0) { - return {}; - } - result.resize(written - 1); - return result; -} - -inline JSStringRef makeJSString(const std::string& value) { - NSString* string = [[NSString alloc] initWithBytes:value.data() - length:value.size() - encoding:NSUTF8StringEncoding]; - if (string == nil) { - return JSStringCreateWithUTF8CString(value.c_str()); - } - - NSUInteger length = [string length]; - std::vector characters(length); - if (length > 0) { - [string getCharacters:characters.data() range:NSMakeRange(0, length)]; - } - [string release]; - return JSStringCreateWithCharacters(characters.data(), length); -} - -inline JSStringRef makeJSString(const char* value) { - return JSStringCreateWithUTF8CString(value != nullptr ? value : ""); -} - -inline std::string valueToUtf8(JSContextRef context, JSValueRef value) { - if (value == nullptr) { - return {}; - } - JSValueRef exception = nullptr; - JSStringRef string = JSValueToStringCopy(context, value, &exception); - if (string == nullptr || exception != nullptr) { - if (string != nullptr) { - JSStringRelease(string); - } - return {}; - } - std::string result = stringToUtf8(string); - JSStringRelease(string); - return result; -} - -inline JSValueRef makeError(JSContextRef context, const std::string& message) { - JSStringRef string = makeJSString(message); - JSValueRef argument = JSValueMakeString(context, string); - JSStringRelease(string); - JSValueRef exception = nullptr; - JSObjectRef error = JSObjectMakeError(context, 1, &argument, &exception); - if (error != nullptr && exception == nullptr) { - return error; - } - return argument; -} - -inline void setException(JSContextRef context, JSValueRef* exception, const std::exception& error) { - if (exception != nullptr) { - *exception = makeError(context, error.what()); - } -} - -struct RuntimeState : RuntimeCleanupRegistry { - explicit RuntimeState(JSGlobalContextRef context) : context(context) {} - - ~RuntimeState() { - if (hostClass != nullptr) { - JSClassRelease(hostClass); - } - if (functionClass != nullptr) { - JSClassRelease(functionClass); - } - if (selectorGroupFunctionClass != nullptr) { - JSClassRelease(selectorGroupFunctionClass); - } - } - - JSGlobalContextRef context = nullptr; - JSClassRef hostClass = nullptr; - JSClassRef functionClass = nullptr; - JSClassRef selectorGroupFunctionClass = nullptr; -}; - -std::shared_ptr stateForContext(JSGlobalContextRef context); -void releaseStateForContext(JSGlobalContextRef context); - -struct ValueStorage { - enum class Kind { - Undefined, - Null, - Bool, - Number, - JSC, - JSCBorrowed, - }; - - explicit ValueStorage(Kind kind) : kind(kind) {} - - ~ValueStorage() { - if (kind == Kind::JSC && context != nullptr && value != nullptr) { - JSValueUnprotect(context, value); - } - } - - Kind kind = Kind::Undefined; - bool boolValue = false; - double numberValue = 0; - JSGlobalContextRef context = nullptr; - JSValueRef value = nullptr; -}; - -template -const void* hostObjectTypeToken() { - static int token = 0; - return &token; -} - -struct HostObjectHolder { - HostObjectHolder(std::shared_ptr state, std::shared_ptr hostObject, - const void* typeToken, bool nativeInstance = false) - : state(std::move(state)), - hostObject(std::move(hostObject)), - typeToken(typeToken), - nativeInstance(nativeInstance) {} - - std::shared_ptr state; - std::shared_ptr hostObject; - const void* typeToken = nullptr; - // Set for wrappers around an ObjC instance, whose JS prototype is allowed to - // shadow a native property. Recorded as a flag rather than derived from - // typeToken: the wrapper class is defined inside an anonymous namespace in - // the engine translation unit, so a type token taken anywhere else names a - // different type and never compares equal. - bool nativeInstance = false; -}; - -struct FunctionHolder { - FunctionHolder(std::shared_ptr state, HostFunctionType callback) - : state(std::move(state)), callback(std::move(callback)) {} - - std::shared_ptr state; - HostFunctionType callback; -}; - -struct ArrayBufferHolder { - explicit ArrayBufferHolder(std::shared_ptr buffer) : buffer(std::move(buffer)) {} - - std::shared_ptr buffer; -}; - -void setFunctionPrototype(JSGlobalContextRef context, JSObjectRef function); - -} // namespace jscengine - -class Runtime { - public: - explicit Runtime(JSGlobalContextRef context) - : state_(jscengine::stateForContext(context)) {} - - explicit Runtime(std::shared_ptr state) : state_(std::move(state)) {} - - JSGlobalContextRef context() const { return state_->context; } - std::shared_ptr state() const { return state_; } - const void* identity() const { return state_.get(); } - void detachState() { state_.reset(); } - - Object global(); - Value evaluateJavaScript(std::shared_ptr buffer, const std::string& sourceURL); - void drainMicrotasks() {} - - private: - std::shared_ptr state_; -}; - -class String { - public: - String() = default; - String(Runtime& runtime, JSStringRef string); - - static String createFromUtf8(Runtime& runtime, const char* value) { - JSStringRef string = jscengine::makeJSString(value); - String result(runtime, string); - JSStringRelease(string); - return result; - } - - static String createFromUtf8(Runtime& runtime, const std::string& value) { - JSStringRef string = jscengine::makeJSString(value); - String result(runtime, string); - JSStringRelease(string); - return result; - } - - static String createFromUtf8(Runtime& runtime, const uint8_t* value, size_t length) { - std::string text(reinterpret_cast(value), length); - return createFromUtf8(runtime, text); - } - - std::string utf8(Runtime& runtime) const; - JSValueRef local(Runtime& runtime) const { return storage_->value; } - operator Value() const; - - private: - friend class Value; - std::shared_ptr storage_; -}; - -class Value { - public: - Value() : kind_(jscengine::ValueStorage::Kind::Undefined) {} - - Value(bool value) : kind_(jscengine::ValueStorage::Kind::Bool), boolValue_(value) {} - - Value(double value) : kind_(jscengine::ValueStorage::Kind::Number), numberValue_(value) {} - - Value(int value) : Value(static_cast(value)) {} - Value(uint32_t value) : Value(static_cast(value)) {} - - Value(Runtime& runtime, const Value& value) { - if (value.kind_ == jscengine::ValueStorage::Kind::JSCBorrowed) { - // Promote borrowed to owned - storage_ = std::make_shared(jscengine::ValueStorage::Kind::JSC); - storage_->context = runtime.context(); - storage_->value = value.borrowedValue_ != nullptr ? value.borrowedValue_ - : JSValueMakeUndefined(runtime.context()); - JSValueProtect(runtime.context(), storage_->value); - kind_ = jscengine::ValueStorage::Kind::JSC; - return; - } - kind_ = value.kind_; - boolValue_ = value.boolValue_; - numberValue_ = value.numberValue_; - borrowedContext_ = value.borrowedContext_; - borrowedValue_ = value.borrowedValue_; - storage_ = value.storage_; - } - Value(Runtime& runtime, Value&& value) - : kind_(value.kind_), - boolValue_(value.boolValue_), - numberValue_(value.numberValue_), - borrowedContext_(value.borrowedContext_), - borrowedValue_(value.borrowedValue_), - storage_(std::move(value.storage_)) {} - Value(Runtime& runtime, const String& value); - Value(Runtime& runtime, const Object& object); - Value(Runtime& runtime, const Function& function); - Value(Runtime& runtime, const Array& array); - Value(Runtime& runtime, const ArrayBuffer& arrayBuffer); - Value(Runtime& runtime, const BigInt& bigint); - Value(Runtime& runtime, JSValueRef value) - : kind_(jscengine::ValueStorage::Kind::JSC), - storage_(std::make_shared(jscengine::ValueStorage::Kind::JSC)) { - storage_->context = runtime.context(); - storage_->value = value != nullptr ? value : JSValueMakeUndefined(runtime.context()); - JSValueProtect(runtime.context(), storage_->value); - } - - static Value borrowed(Runtime& runtime, JSValueRef value) { - Value result; - result.kind_ = jscengine::ValueStorage::Kind::JSCBorrowed; - result.borrowedContext_ = runtime.context(); - result.borrowedValue_ = value != nullptr ? value : JSValueMakeUndefined(runtime.context()); - return result; - } - - static Value undefined() { return Value(); } - static Value null() { - Value value; - value.kind_ = jscengine::ValueStorage::Kind::Null; - return value; - } - - static bool strictEquals(Runtime& runtime, const Value& lhs, - const Value& rhs) { - return JSValueIsStrictEqual(runtime.context(), lhs.local(runtime), - rhs.local(runtime)); - } - - bool isUndefined() const { - return kind_ == jscengine::ValueStorage::Kind::Undefined || - (isJSC() && JSValueIsUndefined(jscContext(), jscValue())); - } - bool isNull() const { - return kind_ == jscengine::ValueStorage::Kind::Null || - (isJSC() && JSValueIsNull(jscContext(), jscValue())); - } - bool isBool() const { - return kind_ == jscengine::ValueStorage::Kind::Bool || - (isJSC() && JSValueIsBoolean(jscContext(), jscValue())); - } - bool getBool() const { - if (kind_ == jscengine::ValueStorage::Kind::Bool) { - return boolValue_; - } - return isJSC() && JSValueToBoolean(jscContext(), jscValue()); - } - bool isNumber() const { - return kind_ == jscengine::ValueStorage::Kind::Number || - (isJSC() && JSValueIsNumber(jscContext(), jscValue())); - } - double getNumber() const { - if (kind_ == jscengine::ValueStorage::Kind::Number) { - return numberValue_; - } - return isJSC() ? JSValueToNumber(jscContext(), jscValue(), nullptr) : 0; - } - - bool isObject() const { return isJSC() && JSValueIsObject(jscContext(), jscValue()); } - bool isString() const { return isJSC() && JSValueIsString(jscContext(), jscValue()); } - bool isBigInt() const { - if (!isJSC()) { - return false; - } - if (__builtin_available(macOS 15.0, iOS 18.0, *)) { - return JSValueIsBigInt(jscContext(), jscValue()); - } - return false; - } - bool isSymbol() const { return isJSC() && JSValueIsSymbol(jscContext(), jscValue()); } - - Object asObject(Runtime& runtime) const; - String asString(Runtime& runtime) const; - BigInt getBigInt(Runtime& runtime) const; - - JSValueRef local(Runtime& runtime) const { - switch (kind_) { - case jscengine::ValueStorage::Kind::Undefined: - return JSValueMakeUndefined(runtime.context()); - case jscengine::ValueStorage::Kind::Null: - return JSValueMakeNull(runtime.context()); - case jscengine::ValueStorage::Kind::Bool: - return JSValueMakeBoolean(runtime.context(), boolValue_); - case jscengine::ValueStorage::Kind::Number: - return JSValueMakeNumber(runtime.context(), numberValue_); - case jscengine::ValueStorage::Kind::JSC: - return storage_->value; - case jscengine::ValueStorage::Kind::JSCBorrowed: - return borrowedValue_; - } - } - - // Access the shared storage (for Object/Function/Array interop) - std::shared_ptr storage() const { return storage_; } - - static Value fromStorage(std::shared_ptr s) { - Value v; - v.kind_ = s->kind; - v.boolValue_ = s->boolValue; - v.numberValue_ = s->numberValue; - v.storage_ = std::move(s); - return v; - } - - private: - friend class Runtime; - friend class Object; - friend class String; - friend class BigInt; - friend class ArrayBuffer; - friend class Function; - friend class Array; - - bool isJSC() const { - return kind_ == jscengine::ValueStorage::Kind::JSC || - kind_ == jscengine::ValueStorage::Kind::JSCBorrowed; - } - JSContextRef jscContext() const { - return kind_ == jscengine::ValueStorage::Kind::JSCBorrowed ? borrowedContext_ - : storage_->context; - } - JSValueRef jscValue() const { - return kind_ == jscengine::ValueStorage::Kind::JSCBorrowed ? borrowedValue_ : storage_->value; - } - - jscengine::ValueStorage::Kind kind_ = jscengine::ValueStorage::Kind::Undefined; - bool boolValue_ = false; - double numberValue_ = 0; - JSGlobalContextRef borrowedContext_ = nullptr; - JSValueRef borrowedValue_ = nullptr; - std::shared_ptr storage_; -}; - -class Object { - public: - Object() = default; - explicit Object(Runtime& runtime) - : storage_(std::make_shared(jscengine::ValueStorage::Kind::JSC)) { - storage_->context = runtime.context(); - storage_->value = JSObjectMake(runtime.context(), nullptr, nullptr); - JSValueProtect(runtime.context(), storage_->value); - } - - static Object fromValueStorage(std::shared_ptr storage) { - Object object; - object.storage_ = std::move(storage); - return object; - } - - template - static Object createFromHostObject(Runtime& runtime, std::shared_ptr host) { - auto baseHost = std::static_pointer_cast(std::move(host)); - return createFromHostObjectWithToken(runtime, std::move(baseHost), - jscengine::hostObjectTypeToken()); - } - - // The wrapper for an ObjC instance. Same object as createFromHostObject - // builds, but marked so property reads defer to the JS prototype chain when - // it carries the name -- V8 gets that from its kNonMasking template; JSC's - // class callback runs before the prototype is consulted, so it has to ask. - template - static Object createNativeInstanceHostObject(Runtime& runtime, std::shared_ptr host) { - auto baseHost = std::static_pointer_cast(std::move(host)); - return createFromHostObjectWithToken(runtime, std::move(baseHost), - jscengine::hostObjectTypeToken(), - /* nativeInstance */ true); - } - - Value getProperty(Runtime& runtime, const char* name) const { - JSStringRef property = jscengine::makeJSString(name); - JSValueRef exception = nullptr; - JSValueRef result = - JSObjectGetProperty(runtime.context(), local(runtime), property, &exception); - JSStringRelease(property); - if (exception != nullptr) { - throw JSError(runtime, jscengine::valueToUtf8(runtime.context(), exception)); - } - return Value(runtime, result); - } - - Value getProperty(Runtime& runtime, const std::string& name) const { - return getProperty(runtime, name.c_str()); - } - - Value getProperty(Runtime& runtime, const Value& key) const { - JSValueRef exception = nullptr; - JSValueRef result = JSObjectGetPropertyForKey(runtime.context(), local(runtime), - key.local(runtime), &exception); - if (exception != nullptr) { - throw JSError(runtime, jscengine::valueToUtf8(runtime.context(), exception)); - } - return Value(runtime, result); - } - - Object getPropertyAsObject(Runtime& runtime, const char* name) const { - return getProperty(runtime, name).asObject(runtime); - } - - Function getPropertyAsFunction(Runtime& runtime, const char* name) const; - - void setProperty(Runtime& runtime, const char* name, const Value& value) { - JSStringRef property = jscengine::makeJSString(name); - JSValueRef exception = nullptr; - JSObjectSetProperty(runtime.context(), local(runtime), property, value.local(runtime), - kJSPropertyAttributeNone, &exception); - JSStringRelease(property); - if (exception != nullptr) { - throw JSError(runtime, jscengine::valueToUtf8(runtime.context(), exception)); - } - } - - void setProperty(Runtime& runtime, const char* name, const String& value) { - setProperty(runtime, name, Value(runtime, value)); - } - void setProperty(Runtime& runtime, const char* name, const Object& value) { - setProperty(runtime, name, Value(runtime, value)); - } - void setProperty(Runtime& runtime, const char* name, const Function& value); - void setProperty(Runtime& runtime, const char* name, const Array& value); - void setProperty(Runtime& runtime, const char* name, const ArrayBuffer& value); - void setProperty(Runtime& runtime, const char* name, bool value) { - setProperty(runtime, name, Value(value)); - } - void setProperty(Runtime& runtime, const char* name, double value) { - setProperty(runtime, name, Value(value)); - } - void setProperty(Runtime& runtime, const std::string& name, const Value& value) { - setProperty(runtime, name.c_str(), value); - } - void setProperty(Runtime& runtime, const Value& key, const Value& value) { - JSValueRef exception = nullptr; - JSObjectSetPropertyForKey(runtime.context(), local(runtime), key.local(runtime), - value.local(runtime), kJSPropertyAttributeNone, &exception); - if (exception != nullptr) { - throw JSError(runtime, jscengine::valueToUtf8(runtime.context(), exception)); - } - } - - bool hasProperty(Runtime& runtime, const char* name) const { - JSStringRef property = jscengine::makeJSString(name); - bool result = JSObjectHasProperty(runtime.context(), local(runtime), property); - JSStringRelease(property); - return result; - } - - bool isFunction(Runtime& runtime) const { - return JSObjectIsFunction(runtime.context(), local(runtime)); - } - - bool isArray(Runtime& runtime) const { - JSStringRef name = jscengine::makeJSString("Array"); - JSValueRef constructorValue = JSObjectGetProperty( - runtime.context(), JSContextGetGlobalObject(runtime.context()), name, nullptr); - JSStringRelease(name); - if (constructorValue == nullptr || !JSValueIsObject(runtime.context(), constructorValue)) { - return false; - } - JSObjectRef constructor = JSValueToObject(runtime.context(), constructorValue, nullptr); - JSValueRef exception = nullptr; - bool result = - JSValueIsInstanceOfConstructor(runtime.context(), local(runtime), constructor, &exception); - return exception == nullptr && result; - } - - bool isArrayBuffer(Runtime& runtime) const { - JSValueRef exception = nullptr; - JSTypedArrayType type = - JSValueGetTypedArrayType(runtime.context(), storage_->value, &exception); - return exception == nullptr && type == kJSTypedArrayTypeArrayBuffer; - } - - Function asFunction(Runtime& runtime) const; - Array getArray(Runtime& runtime) const; - ArrayBuffer getArrayBuffer(Runtime& runtime) const; - Array getPropertyNames(Runtime& runtime) const; - - template - bool isHostObject(Runtime& runtime) const { - auto holder = hostObjectHolder(runtime); - return holder != nullptr && holder->typeToken == jscengine::hostObjectTypeToken(); - } - - template - std::shared_ptr getHostObject(Runtime& runtime) const { - auto holder = hostObjectHolder(runtime); - if (holder == nullptr || holder->typeToken != jscengine::hostObjectTypeToken()) { - return nullptr; - } - return std::static_pointer_cast(holder->hostObject); - } - - JSObjectRef local(Runtime& runtime) const { - return reinterpret_cast(const_cast(storage_->value)); - } - - operator Value() const { return Value::fromStorage(storage_); } - - protected: - friend class Value; - friend class Runtime; - friend class Function; - friend class Array; - friend class ArrayBuffer; - - explicit Object(std::shared_ptr storage) - : storage_(std::move(storage)) {} - - static Object createFromHostObjectWithToken(Runtime& runtime, std::shared_ptr host, - const void* typeToken, - bool nativeInstance = false); - - jscengine::HostObjectHolder* hostObjectHolder(Runtime& runtime) const { - return static_cast(JSObjectGetPrivate(local(runtime))); - } - - std::shared_ptr storage_; -}; - -class Function : public Object { - public: - Function() = default; - explicit Function(Object object) : Object(std::move(object.storage_)) {} - - static Function createFromHostFunction(Runtime& runtime, const PropNameID& name, unsigned int, - HostFunctionType callback); - - Value call(Runtime& runtime, const Value* args, size_t count) const { - std::vector argv; - argv.reserve(count); - for (size_t i = 0; i < count; i++) { - argv.push_back(args[i].local(runtime)); - } - JSValueRef exception = nullptr; - JSValueRef result = JSObjectCallAsFunction( - runtime.context(), local(runtime), JSContextGetGlobalObject(runtime.context()), argv.size(), - argv.empty() ? nullptr : argv.data(), &exception); - if (exception != nullptr) { - throw JSError(runtime, jscengine::valueToUtf8(runtime.context(), exception)); - } - return Value(runtime, result); - } - - Value call(Runtime& runtime) const { - return call(runtime, static_cast(nullptr), 0); - } - Value call(Runtime& runtime, std::nullptr_t, size_t) const { - return call(runtime, static_cast(nullptr), 0); - } - template - Value call(Runtime& runtime, const Value (&args)[N], size_t count) const { - return call(runtime, static_cast(args), count); - } - template - Value call(Runtime& runtime, Args&&... args) const { - Value argv[] = {Value(runtime, std::forward(args))...}; - return call(runtime, static_cast(argv), sizeof...(Args)); - } - - Value callWithThis(Runtime& runtime, const Object& thisObject, const Value* args = nullptr, - size_t count = 0) const { - std::vector argv; - argv.reserve(count); - for (size_t i = 0; i < count; i++) { - argv.push_back(args[i].local(runtime)); - } - JSValueRef exception = nullptr; - JSValueRef result = - JSObjectCallAsFunction(runtime.context(), local(runtime), thisObject.local(runtime), - argv.size(), argv.empty() ? nullptr : argv.data(), &exception); - if (exception != nullptr) { - throw JSError(runtime, jscengine::valueToUtf8(runtime.context(), exception)); - } - return Value(runtime, result); - } - - Value callAsConstructor(Runtime& runtime, const Value* args, size_t count) const { - std::vector argv; - argv.reserve(count); - for (size_t i = 0; i < count; i++) { - argv.push_back(args[i].local(runtime)); - } - JSValueRef exception = nullptr; - JSValueRef result = JSObjectCallAsConstructor(runtime.context(), local(runtime), argv.size(), - argv.empty() ? nullptr : argv.data(), &exception); - if (exception != nullptr) { - throw JSError(runtime, jscengine::valueToUtf8(runtime.context(), exception)); - } - return Value(runtime, result); - } - Value callAsConstructor(Runtime& runtime, std::nullptr_t, size_t) const { - return callAsConstructor(runtime, static_cast(nullptr), 0); - } - template - Value callAsConstructor(Runtime& runtime, const Value (&args)[N], size_t count) const { - return callAsConstructor(runtime, static_cast(args), count); - } - template - Value callAsConstructor(Runtime& runtime, Args&&... args) const { - Value argv[] = {Value(runtime, std::forward(args))...}; - return callAsConstructor(runtime, static_cast(argv), sizeof...(Args)); - } -}; - -class Array : public Object { - public: - explicit Array(Runtime& runtime, size_t size) - : Object(std::make_shared(jscengine::ValueStorage::Kind::JSC)) { - std::vector initial(size, JSValueMakeUndefined(runtime.context())); - JSValueRef exception = nullptr; - storage_->context = runtime.context(); - storage_->value = - JSObjectMakeArray(runtime.context(), initial.size(), initial.data(), &exception); - if (exception != nullptr) { - throw JSError(runtime, jscengine::valueToUtf8(runtime.context(), exception)); - } - JSValueProtect(runtime.context(), storage_->value); - } - - explicit Array(Object object) : Object(std::move(object.storage_)) {} - - size_t size(Runtime& runtime) const { - Value length = getProperty(runtime, "length"); - return length.isNumber() ? static_cast(std::max(0, length.getNumber())) : 0; - } - - Value getValueAtIndex(Runtime& runtime, size_t index) const { - JSValueRef exception = nullptr; - JSValueRef result = JSObjectGetPropertyAtIndex(runtime.context(), local(runtime), - static_cast(index), &exception); - if (exception != nullptr) { - throw JSError(runtime, jscengine::valueToUtf8(runtime.context(), exception)); - } - return Value(runtime, result); - } - - void setValueAtIndex(Runtime& runtime, size_t index, const Value& value) { - JSValueRef exception = nullptr; - JSObjectSetPropertyAtIndex(runtime.context(), local(runtime), static_cast(index), - value.local(runtime), &exception); - if (exception != nullptr) { - throw JSError(runtime, jscengine::valueToUtf8(runtime.context(), exception)); - } - } - void setValueAtIndex(Runtime& runtime, size_t index, const String& value) { - setValueAtIndex(runtime, index, Value(runtime, value)); - } -}; - -class BigInt { - public: - BigInt() = default; - BigInt(Runtime& runtime, JSValueRef value) - : storage_(std::make_shared(jscengine::ValueStorage::Kind::JSC)) { - storage_->context = runtime.context(); - storage_->value = value; - JSValueProtect(runtime.context(), storage_->value); - } - - static BigInt fromInt64(Runtime& runtime, int64_t value) { - JSValueRef exception = nullptr; - JSValueRef result = nullptr; - if (__builtin_available(macOS 15.0, iOS 18.0, *)) { - result = JSBigIntCreateWithInt64(runtime.context(), value, &exception); - } - if (result == nullptr || exception != nullptr) { - result = JSValueMakeNumber(runtime.context(), static_cast(value)); - } - return BigInt(runtime, result); - } - - static BigInt fromUint64(Runtime& runtime, uint64_t value) { - JSValueRef exception = nullptr; - JSValueRef result = nullptr; - if (__builtin_available(macOS 15.0, iOS 18.0, *)) { - result = JSBigIntCreateWithUInt64(runtime.context(), value, &exception); - } - if (result == nullptr || exception != nullptr) { - result = JSValueMakeNumber(runtime.context(), static_cast(value)); - } - return BigInt(runtime, result); - } - - String toString(Runtime& runtime, int) const { - JSValueRef exception = nullptr; - JSStringRef string = JSValueToStringCopy(runtime.context(), local(runtime), &exception); - if (string == nullptr || exception != nullptr) { - throw JSError(runtime, jscengine::valueToUtf8(runtime.context(), exception)); - } - String result(runtime, string); - JSStringRelease(string); - return result; - } - - JSValueRef local(Runtime& runtime) const { return storage_->value; } - - operator Value() const { return Value::fromStorage(storage_); } - - private: - friend class Value; - std::shared_ptr storage_; -}; - -class ArrayBuffer : public Object { - public: - ArrayBuffer(Runtime& runtime, std::shared_ptr buffer) - : Object(std::make_shared(jscengine::ValueStorage::Kind::JSC)) { - auto* holder = new jscengine::ArrayBufferHolder(std::move(buffer)); - JSValueRef exception = nullptr; - storage_->context = runtime.context(); - storage_->value = JSObjectMakeArrayBufferWithBytesNoCopy( - runtime.context(), holder->buffer->data(), holder->buffer->size(), - [](void*, void* deallocatorContext) { - delete static_cast(deallocatorContext); - }, - holder, &exception); - if (exception != nullptr) { - delete holder; - throw JSError(runtime, jscengine::valueToUtf8(runtime.context(), exception)); - } - JSValueProtect(runtime.context(), storage_->value); - } - - explicit ArrayBuffer(Object object) : Object(std::move(object.storage_)) {} - - size_t size(Runtime& runtime) const { - JSValueRef exception = nullptr; - return JSObjectGetArrayBufferByteLength(runtime.context(), local(runtime), &exception); - } - - uint8_t* data(Runtime& runtime) const { - JSValueRef exception = nullptr; - return static_cast( - JSObjectGetArrayBufferBytesPtr(runtime.context(), local(runtime), &exception)); - } -}; -} // namespace engine -} // namespace nativescript - #endif // TARGET_ENGINE_JSC #endif // NATIVESCRIPT_FFI_JSC_NATIVE_API_JSC_RUNTIME_H diff --git a/NativeScript/ffi/objc/jsc/NativeApiJSCHostObjects.mm b/NativeScript/jsi/jsc/JSCHostObjects.cpp similarity index 99% rename from NativeScript/ffi/objc/jsc/NativeApiJSCHostObjects.mm rename to NativeScript/jsi/jsc/JSCHostObjects.cpp index 282658d66..345afe878 100644 --- a/NativeScript/ffi/objc/jsc/NativeApiJSCHostObjects.mm +++ b/NativeScript/jsi/jsc/JSCHostObjects.cpp @@ -1,4 +1,4 @@ -#include "NativeApiJSCRuntime.h" +#include "jsi/jsc/JSCRuntime.h" #include "jsi/shared/NativeApiStackValueArray.h" #ifdef TARGET_ENGINE_JSC diff --git a/NativeScript/ffi/objc/jsc/NativeApiJSCRuntime.mm b/NativeScript/jsi/jsc/JSCRuntime.cpp similarity index 96% rename from NativeScript/ffi/objc/jsc/NativeApiJSCRuntime.mm rename to NativeScript/jsi/jsc/JSCRuntime.cpp index da5aa2ce8..c46e12882 100644 --- a/NativeScript/ffi/objc/jsc/NativeApiJSCRuntime.mm +++ b/NativeScript/jsi/jsc/JSCRuntime.cpp @@ -1,4 +1,4 @@ -#include "NativeApiJSCRuntime.h" +#include "jsi/jsc/JSCRuntime.h" #ifdef TARGET_ENGINE_JSC diff --git a/NativeScript/jsi/jsc/JSCRuntime.h b/NativeScript/jsi/jsc/JSCRuntime.h new file mode 100644 index 000000000..0dd8c1c93 --- /dev/null +++ b/NativeScript/jsi/jsc/JSCRuntime.h @@ -0,0 +1,963 @@ +#ifndef NATIVESCRIPT_JSI_JSC_JSC_RUNTIME_H +#define NATIVESCRIPT_JSI_JSC_JSC_RUNTIME_H + +// JavaScriptCore behind the JSI-shaped `nativescript::engine` API. +// +// This header is platform-neutral: it depends on JavaScriptCore and the C++ +// standard library, and on nothing from Foundation, the Objective-C runtime, +// or the Apple metadata format. Android compiles it as-is. +// +// The Apple bridge's extra baggage lives in +// ffi/objc/jsc/NativeApiJSCRuntime.h, which includes this file. Apple sources +// keep including that path unchanged. + +#ifdef TARGET_ENGINE_JSC + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "jsi/shared/RuntimeCleanupRegistry.h" + +namespace nativescript { +namespace engine { + +class Runtime; +class Value; +class Object; +class Function; +class Array; +class String; +class BigInt; +class ArrayBuffer; + +class JSError : public std::runtime_error { + public: + JSError(Runtime&, const std::string& message) : std::runtime_error(message) {} + explicit JSError(const std::string& message) : std::runtime_error(message) {} +}; + +class StringBuffer { + public: + explicit StringBuffer(std::string value) : value_(std::move(value)) {} + const char* data() const { return value_.data(); } + size_t size() const { return value_.size(); } + + private: + std::string value_; +}; + +class MutableBuffer { + public: + virtual ~MutableBuffer() = default; + virtual size_t size() const = 0; + virtual uint8_t* data() = 0; +}; + +class PropNameID { + public: + PropNameID() = default; + explicit PropNameID(std::string value) : value_(std::move(value)) {} + + static PropNameID forAscii(Runtime&, const char* value) { + return PropNameID(value != nullptr ? value : ""); + } + + static PropNameID forAscii(Runtime&, const std::string& value) { return PropNameID(value); } + + std::string utf8(Runtime&) const { return value_; } + + private: + std::string value_; +}; + +class HostObject { + public: + virtual ~HostObject() = default; + virtual Value get(Runtime& runtime, const PropNameID& name); + virtual bool set(Runtime& runtime, const PropNameID& name, const Value& value); + virtual std::vector getPropertyNames(Runtime& runtime); +}; + +using HostFunctionType = std::function; + +namespace jscengine { + +inline std::string stringToUtf8(JSStringRef string) { + if (string == nullptr) { + return {}; + } + size_t capacity = JSStringGetMaximumUTF8CStringSize(string); + std::string result(capacity, '\0'); + size_t written = JSStringGetUTF8CString(string, result.data(), capacity); + if (written == 0) { + return {}; + } + result.resize(written - 1); + return result; +} + +// Decode strict UTF-8 into UTF-16, returning false on any malformed sequence. +// +// This used to go through NSString, which is why it was the last thing keeping +// the JSC engine layer tied to Foundation. JSStringCreateWithUTF8CString is +// not a substitute on its own: it takes a NUL-terminated C string, so it +// truncates at an embedded U+0000, which is a legal JavaScript character. +inline bool decodeUtf8ToUtf16(const std::string& value, + std::vector& out) { + out.clear(); + out.reserve(value.size()); + + const auto* bytes = reinterpret_cast(value.data()); + const size_t size = value.size(); + + for (size_t i = 0; i < size;) { + const unsigned char lead = bytes[i]; + uint32_t codePoint = 0; + size_t extra = 0; + + if (lead < 0x80) { + codePoint = lead; + } else if ((lead & 0xE0) == 0xC0) { + codePoint = lead & 0x1Fu; + extra = 1; + } else if ((lead & 0xF0) == 0xE0) { + codePoint = lead & 0x0Fu; + extra = 2; + } else if ((lead & 0xF8) == 0xF0) { + codePoint = lead & 0x07u; + extra = 3; + } else { + return false; // continuation byte or 5/6-byte form + } + + if (i + extra >= size) { + return false; // truncated sequence (holds trivially when extra == 0) + } + + for (size_t j = 1; j <= extra; j++) { + const unsigned char continuation = bytes[i + j]; + if ((continuation & 0xC0) != 0x80) { + return false; + } + codePoint = (codePoint << 6) | (continuation & 0x3Fu); + } + + // Reject overlong forms, surrogates encoded directly, and out-of-range. + static constexpr uint32_t kMinForLength[4] = {0x0, 0x80, 0x800, 0x10000}; + if (codePoint < kMinForLength[extra] || codePoint > 0x10FFFF || + (codePoint >= 0xD800 && codePoint <= 0xDFFF)) { + return false; + } + + if (codePoint <= 0xFFFF) { + out.push_back(static_cast(codePoint)); + } else { + codePoint -= 0x10000; + out.push_back(static_cast(0xD800 + (codePoint >> 10))); + out.push_back(static_cast(0xDC00 + (codePoint & 0x3FF))); + } + + i += extra + 1; + } + + return true; +} + +inline JSStringRef makeJSString(const std::string& value) { + std::vector characters; + if (!decodeUtf8ToUtf16(value, characters)) { + // Malformed UTF-8; fall back to whatever JSC makes of it, matching the + // behaviour of the previous NSString-returns-nil path. + return JSStringCreateWithUTF8CString(value.c_str()); + } + return JSStringCreateWithCharacters(characters.data(), characters.size()); +} + +inline JSStringRef makeJSString(const char* value) { + return JSStringCreateWithUTF8CString(value != nullptr ? value : ""); +} + +inline std::string valueToUtf8(JSContextRef context, JSValueRef value) { + if (value == nullptr) { + return {}; + } + JSValueRef exception = nullptr; + JSStringRef string = JSValueToStringCopy(context, value, &exception); + if (string == nullptr || exception != nullptr) { + if (string != nullptr) { + JSStringRelease(string); + } + return {}; + } + std::string result = stringToUtf8(string); + JSStringRelease(string); + return result; +} + +inline JSValueRef makeError(JSContextRef context, const std::string& message) { + JSStringRef string = makeJSString(message); + JSValueRef argument = JSValueMakeString(context, string); + JSStringRelease(string); + JSValueRef exception = nullptr; + JSObjectRef error = JSObjectMakeError(context, 1, &argument, &exception); + if (error != nullptr && exception == nullptr) { + return error; + } + return argument; +} + +inline void setException(JSContextRef context, JSValueRef* exception, const std::exception& error) { + if (exception != nullptr) { + *exception = makeError(context, error.what()); + } +} + +struct RuntimeState : RuntimeCleanupRegistry { + explicit RuntimeState(JSGlobalContextRef context) : context(context) {} + + ~RuntimeState() { + if (hostClass != nullptr) { + JSClassRelease(hostClass); + } + if (functionClass != nullptr) { + JSClassRelease(functionClass); + } + if (selectorGroupFunctionClass != nullptr) { + JSClassRelease(selectorGroupFunctionClass); + } + } + + JSGlobalContextRef context = nullptr; + JSClassRef hostClass = nullptr; + JSClassRef functionClass = nullptr; + JSClassRef selectorGroupFunctionClass = nullptr; +}; + +std::shared_ptr stateForContext(JSGlobalContextRef context); +void releaseStateForContext(JSGlobalContextRef context); + +struct ValueStorage { + enum class Kind { + Undefined, + Null, + Bool, + Number, + JSC, + JSCBorrowed, + }; + + explicit ValueStorage(Kind kind) : kind(kind) {} + + ~ValueStorage() { + if (kind == Kind::JSC && context != nullptr && value != nullptr) { + JSValueUnprotect(context, value); + } + } + + Kind kind = Kind::Undefined; + bool boolValue = false; + double numberValue = 0; + JSGlobalContextRef context = nullptr; + JSValueRef value = nullptr; +}; + +template +const void* hostObjectTypeToken() { + static int token = 0; + return &token; +} + +struct HostObjectHolder { + HostObjectHolder(std::shared_ptr state, std::shared_ptr hostObject, + const void* typeToken, bool nativeInstance = false) + : state(std::move(state)), + hostObject(std::move(hostObject)), + typeToken(typeToken), + nativeInstance(nativeInstance) {} + + std::shared_ptr state; + std::shared_ptr hostObject; + const void* typeToken = nullptr; + // Set for wrappers around an ObjC instance, whose JS prototype is allowed to + // shadow a native property. Recorded as a flag rather than derived from + // typeToken: the wrapper class is defined inside an anonymous namespace in + // the engine translation unit, so a type token taken anywhere else names a + // different type and never compares equal. + bool nativeInstance = false; +}; + +struct FunctionHolder { + FunctionHolder(std::shared_ptr state, HostFunctionType callback) + : state(std::move(state)), callback(std::move(callback)) {} + + std::shared_ptr state; + HostFunctionType callback; +}; + +struct ArrayBufferHolder { + explicit ArrayBufferHolder(std::shared_ptr buffer) : buffer(std::move(buffer)) {} + + std::shared_ptr buffer; +}; + +void setFunctionPrototype(JSGlobalContextRef context, JSObjectRef function); + +} // namespace jscengine + +class Runtime { + public: + explicit Runtime(JSGlobalContextRef context) + : state_(jscengine::stateForContext(context)) {} + + explicit Runtime(std::shared_ptr state) : state_(std::move(state)) {} + + JSGlobalContextRef context() const { return state_->context; } + std::shared_ptr state() const { return state_; } + const void* identity() const { return state_.get(); } + void detachState() { state_.reset(); } + + Object global(); + Value evaluateJavaScript(std::shared_ptr buffer, const std::string& sourceURL); + void drainMicrotasks() {} + + private: + std::shared_ptr state_; +}; + +class String { + public: + String() = default; + String(Runtime& runtime, JSStringRef string); + + static String createFromUtf8(Runtime& runtime, const char* value) { + JSStringRef string = jscengine::makeJSString(value); + String result(runtime, string); + JSStringRelease(string); + return result; + } + + static String createFromUtf8(Runtime& runtime, const std::string& value) { + JSStringRef string = jscengine::makeJSString(value); + String result(runtime, string); + JSStringRelease(string); + return result; + } + + static String createFromUtf8(Runtime& runtime, const uint8_t* value, size_t length) { + std::string text(reinterpret_cast(value), length); + return createFromUtf8(runtime, text); + } + + std::string utf8(Runtime& runtime) const; + JSValueRef local(Runtime& runtime) const { return storage_->value; } + operator Value() const; + + private: + friend class Value; + std::shared_ptr storage_; +}; + +class Value { + public: + Value() : kind_(jscengine::ValueStorage::Kind::Undefined) {} + + Value(bool value) : kind_(jscengine::ValueStorage::Kind::Bool), boolValue_(value) {} + + Value(double value) : kind_(jscengine::ValueStorage::Kind::Number), numberValue_(value) {} + + Value(int value) : Value(static_cast(value)) {} + Value(uint32_t value) : Value(static_cast(value)) {} + + Value(Runtime& runtime, const Value& value) { + if (value.kind_ == jscengine::ValueStorage::Kind::JSCBorrowed) { + // Promote borrowed to owned + storage_ = std::make_shared(jscengine::ValueStorage::Kind::JSC); + storage_->context = runtime.context(); + storage_->value = value.borrowedValue_ != nullptr ? value.borrowedValue_ + : JSValueMakeUndefined(runtime.context()); + JSValueProtect(runtime.context(), storage_->value); + kind_ = jscengine::ValueStorage::Kind::JSC; + return; + } + kind_ = value.kind_; + boolValue_ = value.boolValue_; + numberValue_ = value.numberValue_; + borrowedContext_ = value.borrowedContext_; + borrowedValue_ = value.borrowedValue_; + storage_ = value.storage_; + } + Value(Runtime& runtime, Value&& value) + : kind_(value.kind_), + boolValue_(value.boolValue_), + numberValue_(value.numberValue_), + borrowedContext_(value.borrowedContext_), + borrowedValue_(value.borrowedValue_), + storage_(std::move(value.storage_)) {} + Value(Runtime& runtime, const String& value); + Value(Runtime& runtime, const Object& object); + Value(Runtime& runtime, const Function& function); + Value(Runtime& runtime, const Array& array); + Value(Runtime& runtime, const ArrayBuffer& arrayBuffer); + Value(Runtime& runtime, const BigInt& bigint); + Value(Runtime& runtime, JSValueRef value) + : kind_(jscengine::ValueStorage::Kind::JSC), + storage_(std::make_shared(jscengine::ValueStorage::Kind::JSC)) { + storage_->context = runtime.context(); + storage_->value = value != nullptr ? value : JSValueMakeUndefined(runtime.context()); + JSValueProtect(runtime.context(), storage_->value); + } + + static Value borrowed(Runtime& runtime, JSValueRef value) { + Value result; + result.kind_ = jscengine::ValueStorage::Kind::JSCBorrowed; + result.borrowedContext_ = runtime.context(); + result.borrowedValue_ = value != nullptr ? value : JSValueMakeUndefined(runtime.context()); + return result; + } + + static Value undefined() { return Value(); } + static Value null() { + Value value; + value.kind_ = jscengine::ValueStorage::Kind::Null; + return value; + } + + static bool strictEquals(Runtime& runtime, const Value& lhs, + const Value& rhs) { + return JSValueIsStrictEqual(runtime.context(), lhs.local(runtime), + rhs.local(runtime)); + } + + bool isUndefined() const { + return kind_ == jscengine::ValueStorage::Kind::Undefined || + (isJSC() && JSValueIsUndefined(jscContext(), jscValue())); + } + bool isNull() const { + return kind_ == jscengine::ValueStorage::Kind::Null || + (isJSC() && JSValueIsNull(jscContext(), jscValue())); + } + bool isBool() const { + return kind_ == jscengine::ValueStorage::Kind::Bool || + (isJSC() && JSValueIsBoolean(jscContext(), jscValue())); + } + bool getBool() const { + if (kind_ == jscengine::ValueStorage::Kind::Bool) { + return boolValue_; + } + return isJSC() && JSValueToBoolean(jscContext(), jscValue()); + } + bool isNumber() const { + return kind_ == jscengine::ValueStorage::Kind::Number || + (isJSC() && JSValueIsNumber(jscContext(), jscValue())); + } + double getNumber() const { + if (kind_ == jscengine::ValueStorage::Kind::Number) { + return numberValue_; + } + return isJSC() ? JSValueToNumber(jscContext(), jscValue(), nullptr) : 0; + } + + bool isObject() const { return isJSC() && JSValueIsObject(jscContext(), jscValue()); } + bool isString() const { return isJSC() && JSValueIsString(jscContext(), jscValue()); } + bool isBigInt() const { + if (!isJSC()) { + return false; + } + if (__builtin_available(macOS 15.0, iOS 18.0, *)) { + return JSValueIsBigInt(jscContext(), jscValue()); + } + return false; + } + bool isSymbol() const { return isJSC() && JSValueIsSymbol(jscContext(), jscValue()); } + + Object asObject(Runtime& runtime) const; + String asString(Runtime& runtime) const; + BigInt getBigInt(Runtime& runtime) const; + + JSValueRef local(Runtime& runtime) const { + switch (kind_) { + case jscengine::ValueStorage::Kind::Undefined: + return JSValueMakeUndefined(runtime.context()); + case jscengine::ValueStorage::Kind::Null: + return JSValueMakeNull(runtime.context()); + case jscengine::ValueStorage::Kind::Bool: + return JSValueMakeBoolean(runtime.context(), boolValue_); + case jscengine::ValueStorage::Kind::Number: + return JSValueMakeNumber(runtime.context(), numberValue_); + case jscengine::ValueStorage::Kind::JSC: + return storage_->value; + case jscengine::ValueStorage::Kind::JSCBorrowed: + return borrowedValue_; + } + } + + // Access the shared storage (for Object/Function/Array interop) + std::shared_ptr storage() const { return storage_; } + + static Value fromStorage(std::shared_ptr s) { + Value v; + v.kind_ = s->kind; + v.boolValue_ = s->boolValue; + v.numberValue_ = s->numberValue; + v.storage_ = std::move(s); + return v; + } + + private: + friend class Runtime; + friend class Object; + friend class String; + friend class BigInt; + friend class ArrayBuffer; + friend class Function; + friend class Array; + + bool isJSC() const { + return kind_ == jscengine::ValueStorage::Kind::JSC || + kind_ == jscengine::ValueStorage::Kind::JSCBorrowed; + } + JSContextRef jscContext() const { + return kind_ == jscengine::ValueStorage::Kind::JSCBorrowed ? borrowedContext_ + : storage_->context; + } + JSValueRef jscValue() const { + return kind_ == jscengine::ValueStorage::Kind::JSCBorrowed ? borrowedValue_ : storage_->value; + } + + jscengine::ValueStorage::Kind kind_ = jscengine::ValueStorage::Kind::Undefined; + bool boolValue_ = false; + double numberValue_ = 0; + JSGlobalContextRef borrowedContext_ = nullptr; + JSValueRef borrowedValue_ = nullptr; + std::shared_ptr storage_; +}; + +class Object { + public: + Object() = default; + explicit Object(Runtime& runtime) + : storage_(std::make_shared(jscengine::ValueStorage::Kind::JSC)) { + storage_->context = runtime.context(); + storage_->value = JSObjectMake(runtime.context(), nullptr, nullptr); + JSValueProtect(runtime.context(), storage_->value); + } + + static Object fromValueStorage(std::shared_ptr storage) { + Object object; + object.storage_ = std::move(storage); + return object; + } + + template + static Object createFromHostObject(Runtime& runtime, std::shared_ptr host) { + auto baseHost = std::static_pointer_cast(std::move(host)); + return createFromHostObjectWithToken(runtime, std::move(baseHost), + jscengine::hostObjectTypeToken()); + } + + // The wrapper for an ObjC instance. Same object as createFromHostObject + // builds, but marked so property reads defer to the JS prototype chain when + // it carries the name -- V8 gets that from its kNonMasking template; JSC's + // class callback runs before the prototype is consulted, so it has to ask. + template + static Object createNativeInstanceHostObject(Runtime& runtime, std::shared_ptr host) { + auto baseHost = std::static_pointer_cast(std::move(host)); + return createFromHostObjectWithToken(runtime, std::move(baseHost), + jscengine::hostObjectTypeToken(), + /* nativeInstance */ true); + } + + Value getProperty(Runtime& runtime, const char* name) const { + JSStringRef property = jscengine::makeJSString(name); + JSValueRef exception = nullptr; + JSValueRef result = + JSObjectGetProperty(runtime.context(), local(runtime), property, &exception); + JSStringRelease(property); + if (exception != nullptr) { + throw JSError(runtime, jscengine::valueToUtf8(runtime.context(), exception)); + } + return Value(runtime, result); + } + + Value getProperty(Runtime& runtime, const std::string& name) const { + return getProperty(runtime, name.c_str()); + } + + Value getProperty(Runtime& runtime, const Value& key) const { + JSValueRef exception = nullptr; + JSValueRef result = JSObjectGetPropertyForKey(runtime.context(), local(runtime), + key.local(runtime), &exception); + if (exception != nullptr) { + throw JSError(runtime, jscengine::valueToUtf8(runtime.context(), exception)); + } + return Value(runtime, result); + } + + Object getPropertyAsObject(Runtime& runtime, const char* name) const { + return getProperty(runtime, name).asObject(runtime); + } + + Function getPropertyAsFunction(Runtime& runtime, const char* name) const; + + void setProperty(Runtime& runtime, const char* name, const Value& value) { + JSStringRef property = jscengine::makeJSString(name); + JSValueRef exception = nullptr; + JSObjectSetProperty(runtime.context(), local(runtime), property, value.local(runtime), + kJSPropertyAttributeNone, &exception); + JSStringRelease(property); + if (exception != nullptr) { + throw JSError(runtime, jscengine::valueToUtf8(runtime.context(), exception)); + } + } + + void setProperty(Runtime& runtime, const char* name, const String& value) { + setProperty(runtime, name, Value(runtime, value)); + } + void setProperty(Runtime& runtime, const char* name, const Object& value) { + setProperty(runtime, name, Value(runtime, value)); + } + void setProperty(Runtime& runtime, const char* name, const Function& value); + void setProperty(Runtime& runtime, const char* name, const Array& value); + void setProperty(Runtime& runtime, const char* name, const ArrayBuffer& value); + void setProperty(Runtime& runtime, const char* name, bool value) { + setProperty(runtime, name, Value(value)); + } + void setProperty(Runtime& runtime, const char* name, double value) { + setProperty(runtime, name, Value(value)); + } + void setProperty(Runtime& runtime, const std::string& name, const Value& value) { + setProperty(runtime, name.c_str(), value); + } + void setProperty(Runtime& runtime, const Value& key, const Value& value) { + JSValueRef exception = nullptr; + JSObjectSetPropertyForKey(runtime.context(), local(runtime), key.local(runtime), + value.local(runtime), kJSPropertyAttributeNone, &exception); + if (exception != nullptr) { + throw JSError(runtime, jscengine::valueToUtf8(runtime.context(), exception)); + } + } + + bool hasProperty(Runtime& runtime, const char* name) const { + JSStringRef property = jscengine::makeJSString(name); + bool result = JSObjectHasProperty(runtime.context(), local(runtime), property); + JSStringRelease(property); + return result; + } + + bool isFunction(Runtime& runtime) const { + return JSObjectIsFunction(runtime.context(), local(runtime)); + } + + bool isArray(Runtime& runtime) const { + JSStringRef name = jscengine::makeJSString("Array"); + JSValueRef constructorValue = JSObjectGetProperty( + runtime.context(), JSContextGetGlobalObject(runtime.context()), name, nullptr); + JSStringRelease(name); + if (constructorValue == nullptr || !JSValueIsObject(runtime.context(), constructorValue)) { + return false; + } + JSObjectRef constructor = JSValueToObject(runtime.context(), constructorValue, nullptr); + JSValueRef exception = nullptr; + bool result = + JSValueIsInstanceOfConstructor(runtime.context(), local(runtime), constructor, &exception); + return exception == nullptr && result; + } + + bool isArrayBuffer(Runtime& runtime) const { + JSValueRef exception = nullptr; + JSTypedArrayType type = + JSValueGetTypedArrayType(runtime.context(), storage_->value, &exception); + return exception == nullptr && type == kJSTypedArrayTypeArrayBuffer; + } + + Function asFunction(Runtime& runtime) const; + Array getArray(Runtime& runtime) const; + ArrayBuffer getArrayBuffer(Runtime& runtime) const; + Array getPropertyNames(Runtime& runtime) const; + + template + bool isHostObject(Runtime& runtime) const { + auto holder = hostObjectHolder(runtime); + return holder != nullptr && holder->typeToken == jscengine::hostObjectTypeToken(); + } + + template + std::shared_ptr getHostObject(Runtime& runtime) const { + auto holder = hostObjectHolder(runtime); + if (holder == nullptr || holder->typeToken != jscengine::hostObjectTypeToken()) { + return nullptr; + } + return std::static_pointer_cast(holder->hostObject); + } + + JSObjectRef local(Runtime& runtime) const { + return reinterpret_cast(const_cast(storage_->value)); + } + + operator Value() const { return Value::fromStorage(storage_); } + + protected: + friend class Value; + friend class Runtime; + friend class Function; + friend class Array; + friend class ArrayBuffer; + + explicit Object(std::shared_ptr storage) + : storage_(std::move(storage)) {} + + static Object createFromHostObjectWithToken(Runtime& runtime, std::shared_ptr host, + const void* typeToken, + bool nativeInstance = false); + + jscengine::HostObjectHolder* hostObjectHolder(Runtime& runtime) const { + return static_cast(JSObjectGetPrivate(local(runtime))); + } + + std::shared_ptr storage_; +}; + +class Function : public Object { + public: + Function() = default; + explicit Function(Object object) : Object(std::move(object.storage_)) {} + + static Function createFromHostFunction(Runtime& runtime, const PropNameID& name, unsigned int, + HostFunctionType callback); + + Value call(Runtime& runtime, const Value* args, size_t count) const { + std::vector argv; + argv.reserve(count); + for (size_t i = 0; i < count; i++) { + argv.push_back(args[i].local(runtime)); + } + JSValueRef exception = nullptr; + JSValueRef result = JSObjectCallAsFunction( + runtime.context(), local(runtime), JSContextGetGlobalObject(runtime.context()), argv.size(), + argv.empty() ? nullptr : argv.data(), &exception); + if (exception != nullptr) { + throw JSError(runtime, jscengine::valueToUtf8(runtime.context(), exception)); + } + return Value(runtime, result); + } + + Value call(Runtime& runtime) const { + return call(runtime, static_cast(nullptr), 0); + } + Value call(Runtime& runtime, std::nullptr_t, size_t) const { + return call(runtime, static_cast(nullptr), 0); + } + template + Value call(Runtime& runtime, const Value (&args)[N], size_t count) const { + return call(runtime, static_cast(args), count); + } + template + Value call(Runtime& runtime, Args&&... args) const { + Value argv[] = {Value(runtime, std::forward(args))...}; + return call(runtime, static_cast(argv), sizeof...(Args)); + } + + Value callWithThis(Runtime& runtime, const Object& thisObject, const Value* args = nullptr, + size_t count = 0) const { + std::vector argv; + argv.reserve(count); + for (size_t i = 0; i < count; i++) { + argv.push_back(args[i].local(runtime)); + } + JSValueRef exception = nullptr; + JSValueRef result = + JSObjectCallAsFunction(runtime.context(), local(runtime), thisObject.local(runtime), + argv.size(), argv.empty() ? nullptr : argv.data(), &exception); + if (exception != nullptr) { + throw JSError(runtime, jscengine::valueToUtf8(runtime.context(), exception)); + } + return Value(runtime, result); + } + + Value callAsConstructor(Runtime& runtime, const Value* args, size_t count) const { + std::vector argv; + argv.reserve(count); + for (size_t i = 0; i < count; i++) { + argv.push_back(args[i].local(runtime)); + } + JSValueRef exception = nullptr; + JSValueRef result = JSObjectCallAsConstructor(runtime.context(), local(runtime), argv.size(), + argv.empty() ? nullptr : argv.data(), &exception); + if (exception != nullptr) { + throw JSError(runtime, jscengine::valueToUtf8(runtime.context(), exception)); + } + return Value(runtime, result); + } + Value callAsConstructor(Runtime& runtime, std::nullptr_t, size_t) const { + return callAsConstructor(runtime, static_cast(nullptr), 0); + } + template + Value callAsConstructor(Runtime& runtime, const Value (&args)[N], size_t count) const { + return callAsConstructor(runtime, static_cast(args), count); + } + template + Value callAsConstructor(Runtime& runtime, Args&&... args) const { + Value argv[] = {Value(runtime, std::forward(args))...}; + return callAsConstructor(runtime, static_cast(argv), sizeof...(Args)); + } +}; + +class Array : public Object { + public: + explicit Array(Runtime& runtime, size_t size) + : Object(std::make_shared(jscengine::ValueStorage::Kind::JSC)) { + std::vector initial(size, JSValueMakeUndefined(runtime.context())); + JSValueRef exception = nullptr; + storage_->context = runtime.context(); + storage_->value = + JSObjectMakeArray(runtime.context(), initial.size(), initial.data(), &exception); + if (exception != nullptr) { + throw JSError(runtime, jscengine::valueToUtf8(runtime.context(), exception)); + } + JSValueProtect(runtime.context(), storage_->value); + } + + explicit Array(Object object) : Object(std::move(object.storage_)) {} + + size_t size(Runtime& runtime) const { + Value length = getProperty(runtime, "length"); + return length.isNumber() ? static_cast(std::max(0, length.getNumber())) : 0; + } + + Value getValueAtIndex(Runtime& runtime, size_t index) const { + JSValueRef exception = nullptr; + JSValueRef result = JSObjectGetPropertyAtIndex(runtime.context(), local(runtime), + static_cast(index), &exception); + if (exception != nullptr) { + throw JSError(runtime, jscengine::valueToUtf8(runtime.context(), exception)); + } + return Value(runtime, result); + } + + void setValueAtIndex(Runtime& runtime, size_t index, const Value& value) { + JSValueRef exception = nullptr; + JSObjectSetPropertyAtIndex(runtime.context(), local(runtime), static_cast(index), + value.local(runtime), &exception); + if (exception != nullptr) { + throw JSError(runtime, jscengine::valueToUtf8(runtime.context(), exception)); + } + } + void setValueAtIndex(Runtime& runtime, size_t index, const String& value) { + setValueAtIndex(runtime, index, Value(runtime, value)); + } +}; + +class BigInt { + public: + BigInt() = default; + BigInt(Runtime& runtime, JSValueRef value) + : storage_(std::make_shared(jscengine::ValueStorage::Kind::JSC)) { + storage_->context = runtime.context(); + storage_->value = value; + JSValueProtect(runtime.context(), storage_->value); + } + + static BigInt fromInt64(Runtime& runtime, int64_t value) { + JSValueRef exception = nullptr; + JSValueRef result = nullptr; + if (__builtin_available(macOS 15.0, iOS 18.0, *)) { + result = JSBigIntCreateWithInt64(runtime.context(), value, &exception); + } + if (result == nullptr || exception != nullptr) { + result = JSValueMakeNumber(runtime.context(), static_cast(value)); + } + return BigInt(runtime, result); + } + + static BigInt fromUint64(Runtime& runtime, uint64_t value) { + JSValueRef exception = nullptr; + JSValueRef result = nullptr; + if (__builtin_available(macOS 15.0, iOS 18.0, *)) { + result = JSBigIntCreateWithUInt64(runtime.context(), value, &exception); + } + if (result == nullptr || exception != nullptr) { + result = JSValueMakeNumber(runtime.context(), static_cast(value)); + } + return BigInt(runtime, result); + } + + String toString(Runtime& runtime, int) const { + JSValueRef exception = nullptr; + JSStringRef string = JSValueToStringCopy(runtime.context(), local(runtime), &exception); + if (string == nullptr || exception != nullptr) { + throw JSError(runtime, jscengine::valueToUtf8(runtime.context(), exception)); + } + String result(runtime, string); + JSStringRelease(string); + return result; + } + + JSValueRef local(Runtime& runtime) const { return storage_->value; } + + operator Value() const { return Value::fromStorage(storage_); } + + private: + friend class Value; + std::shared_ptr storage_; +}; + +class ArrayBuffer : public Object { + public: + ArrayBuffer(Runtime& runtime, std::shared_ptr buffer) + : Object(std::make_shared(jscengine::ValueStorage::Kind::JSC)) { + auto* holder = new jscengine::ArrayBufferHolder(std::move(buffer)); + JSValueRef exception = nullptr; + storage_->context = runtime.context(); + storage_->value = JSObjectMakeArrayBufferWithBytesNoCopy( + runtime.context(), holder->buffer->data(), holder->buffer->size(), + [](void*, void* deallocatorContext) { + delete static_cast(deallocatorContext); + }, + holder, &exception); + if (exception != nullptr) { + delete holder; + throw JSError(runtime, jscengine::valueToUtf8(runtime.context(), exception)); + } + JSValueProtect(runtime.context(), storage_->value); + } + + explicit ArrayBuffer(Object object) : Object(std::move(object.storage_)) {} + + size_t size(Runtime& runtime) const { + JSValueRef exception = nullptr; + return JSObjectGetArrayBufferByteLength(runtime.context(), local(runtime), &exception); + } + + uint8_t* data(Runtime& runtime) const { + JSValueRef exception = nullptr; + return static_cast( + JSObjectGetArrayBufferBytesPtr(runtime.context(), local(runtime), &exception)); + } +}; +} // namespace engine +} // namespace nativescript + +#endif // TARGET_ENGINE_JSC + +#endif // NATIVESCRIPT_JSI_JSC_JSC_RUNTIME_H diff --git a/NativeScript/ffi/objc/jsc/NativeApiJSCValue.mm b/NativeScript/jsi/jsc/JSCValue.cpp similarity index 99% rename from NativeScript/ffi/objc/jsc/NativeApiJSCValue.mm rename to NativeScript/jsi/jsc/JSCValue.cpp index f66913761..5db45d4a5 100644 --- a/NativeScript/ffi/objc/jsc/NativeApiJSCValue.mm +++ b/NativeScript/jsi/jsc/JSCValue.cpp @@ -1,4 +1,4 @@ -#include "NativeApiJSCRuntime.h" +#include "jsi/jsc/JSCRuntime.h" #ifdef TARGET_ENGINE_JSC From ca14ff2a6036f7dd4dace9528b8b2fcdb9f24896 Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Thu, 30 Jul 2026 23:23:01 +0500 Subject: [PATCH 5/7] refactor(jsi): share the Hermes contract header and the GSD lookup side Hermes is structurally unlike the other three engines: it exposes the real facebook::jsi API instead of reimplementing the nativescript::engine shape, so there is no engine layer to extract. Its public header was already neutral. What moves is therefore small. jsi/hermes/NativeApiJsi.h - the install/create declaration. Android's Hermes also exposes facebook::jsi, so this is shareable verbatim. jsi/shared/PreparedSignatureDispatch.h - the GSD lookup side. The second one completes the split started in the first commit of this branch. Given a dispatch id, this header finds the generated trampoline; it needs only the hashing/lookup core, never the metadata walk that computes the ids. So the line falls in the same place as before: mechanism shared, metadata traversal per-platform. Its include changed from SignatureDispatchCore.h to SignatureHashing.h, which is what it actually used. Forwarding headers at both old paths, so no Apple include changed. The Apple-side PreparedSignatureDispatch.h forwarder still pulls in SignatureDispatchCore.h first, because its callers have always had that transitively. Verified: iOS TestRunner on QuickJS -- which includes the moved GSD header -- 713 specs, 0 failures. The Hermes target compiles, links, and launches. Its suite cannot be run: Apple Hermes hangs after ~10 specs. That is pre-existing and unrelated to this branch -- a build at 15e5418c, the commit before the Android-updates work began, hangs identically. Worth fixing, but as its own piece of work. (cherry picked from commit 66a26288a8490c59cd31b9d4afa6390b54810f64) --- NativeScript/ffi/objc/hermes/NativeApiJsi.h | 67 ++------------ .../objc/shared/PreparedSignatureDispatch.h | 84 ++---------------- NativeScript/jsi/hermes/NativeApiJsi.h | 37 ++++++++ .../jsi/shared/PreparedSignatureDispatch.h | 87 +++++++++++++++++++ 4 files changed, 142 insertions(+), 133 deletions(-) create mode 100644 NativeScript/jsi/hermes/NativeApiJsi.h create mode 100644 NativeScript/jsi/shared/PreparedSignatureDispatch.h diff --git a/NativeScript/ffi/objc/hermes/NativeApiJsi.h b/NativeScript/ffi/objc/hermes/NativeApiJsi.h index f2bd4d1f1..b37401f30 100644 --- a/NativeScript/ffi/objc/hermes/NativeApiJsi.h +++ b/NativeScript/ffi/objc/hermes/NativeApiJsi.h @@ -1,62 +1,13 @@ -#ifndef NATIVE_API_JSI_H -#define NATIVE_API_JSI_H +#ifndef NATIVE_API_JSI_H_FFI_FORWARD +#define NATIVE_API_JSI_H_FFI_FORWARD -#include - -#include "ffi/objc/shared/NativeApiBackendConfig.h" - -namespace nativescript { - -using NativeApiJsiScheduler = NativeApiBackendScheduler; -using NativeApiJsiConfig = NativeApiBackendConfig; - -facebook::jsi::Object CreateNativeApiJSI( - facebook::jsi::Runtime& runtime, - const NativeApiJsiConfig& config = NativeApiJsiConfig{}); - -void InstallNativeApiJSI( - facebook::jsi::Runtime& runtime, - const NativeApiJsiConfig& config = NativeApiJsiConfig{}); - -// M1 (ARCHITECTURE.md §4.3): the two ObjC<->JSI helpers that make the -// by-reference Fabric handoff possible; "the Fabric boundary must hand JS -// a real bridge-wrapped object, not a string handle" -// (CLEANUP_AND_REARCHITECTURE_PLAN.md §2.0). Both wrap the SAME -// NativeApiObjectHostObject mechanism every other native object crossing in -// this bridge already uses (Object.mm/Class.mm); so a wrapped value -// round-trips through the identical `nativeValue(...)`-style method dispatch -// as any other bridged object, not a bespoke RPC. -// -// `object`/the return value are `void*`-typed ObjC `id`s, kept untyped here -// (not `id`) so this header stays includable from a plain C++ translation -// unit that never imports Objective-C (e.g. runtime/apple/Runtime.cpp, -// which includes this header under `#ifdef TARGET_ENGINE_HERMES` without -// itself being compiled as Objective-C++); the same convention -// NativeApiBackendConfig.h already follows. -// -// Only implemented for the Hermes backend (this header/its .mm are -// Hermes-only, ffi/objc/hermes/); RN only ever uses Hermes, so this does not -// touch the V8/JSC/QuickJS engine backends or their standalone builds. +// Moved to jsi/hermes/NativeApiJsi.h. Hermes is the one backend that exposes +// the real facebook::jsi API rather than reimplementing the nativescript +// engine shape, and this declaration is already platform-neutral, so Android's +// Hermes backend can share it verbatim. // -// If `ownsObject` is true, the wrapper takes over a +1 retain already held -// by the caller (matching `makeNativeObjectValue`'s `ownsObject` semantics -// used everywhere else in the bridge); if false, the wrapper retains its own -// reference and the caller's reference is untouched. -facebook::jsi::Value NativeScriptWrapNativeObject(facebook::jsi::Runtime& runtime, - void* object, - bool ownsObject = false); - -// Reverse direction: given a JSI value produced by NativeScriptWrapNativeObject -// (or any other native-object-wrapping mechanism the bridge already uses -- -// NativeApiObjectHostObject, NativeApiPointerHostObject, -// NativeApiReferenceHostObject), returns the underlying native pointer, or -// nullptr if `value` does not wrap a live native object. -void* NativeScriptUnwrapNativeObject(facebook::jsi::Runtime& runtime, - const facebook::jsi::Value& value); - -} // namespace nativescript +// This forwarding header keeps the Apple include paths unchanged. -extern "C" void NativeScriptInstallNativeApiJSI( - facebook::jsi::Runtime* runtime, const char* metadataPath); +#include "jsi/hermes/NativeApiJsi.h" -#endif // NATIVE_API_JSI_H +#endif // NATIVE_API_JSI_H_FFI_FORWARD diff --git a/NativeScript/ffi/objc/shared/PreparedSignatureDispatch.h b/NativeScript/ffi/objc/shared/PreparedSignatureDispatch.h index c941006fe..4a2577ab7 100644 --- a/NativeScript/ffi/objc/shared/PreparedSignatureDispatch.h +++ b/NativeScript/ffi/objc/shared/PreparedSignatureDispatch.h @@ -1,80 +1,14 @@ #ifndef NATIVESCRIPT_FFI_SHARED_PREPARED_SIGNATURE_DISPATCH_H #define NATIVESCRIPT_FFI_SHARED_PREPARED_SIGNATURE_DISPATCH_H -#include "SignatureDispatchCore.h" - -#ifndef NS_GSD_BACKEND_PREPARED -#define NS_GSD_BACKEND_PREPARED 0 -#endif - -#ifndef NS_GSD_BACKEND_HERMES -#define NS_GSD_BACKEND_HERMES 0 -#endif - -#ifndef NS_GSD_BACKEND_NAPI -#define NS_GSD_BACKEND_NAPI 0 -#endif - -#ifndef NS_HAS_GENERATED_SIGNATURE_DISPATCH -#define NS_HAS_GENERATED_SIGNATURE_DISPATCH 0 -#endif - -#define NS_REQUIRES_GENERATED_SIGNATURE_DISPATCH \ - (NS_GSD_BACKEND_HERMES || NS_GSD_BACKEND_NAPI || NS_GSD_BACKEND_PREPARED) - -#if NS_REQUIRES_GENERATED_SIGNATURE_DISPATCH && \ - !NS_HAS_GENERATED_SIGNATURE_DISPATCH -#error GeneratedSignatureDispatch.inc did not enable this generated signature dispatch backend. -#endif - -#if !NS_HAS_GENERATED_SIGNATURE_DISPATCH -namespace nativescript { -inline constexpr ObjCDispatchEntry kGeneratedObjCDispatchEntries[] = { - {0, nullptr}}; -inline constexpr CFunctionDispatchEntry kGeneratedCFunctionDispatchEntries[] = { - {0, nullptr}}; -inline constexpr BlockDispatchEntry kGeneratedBlockDispatchEntries[] = { - {0, nullptr}}; -} // namespace nativescript -#endif - -namespace nativescript { - -inline ObjCPreparedInvoker lookupObjCPreparedInvoker(uint64_t dispatchId) { - if (!isGeneratedDispatchEnabled()) { - return nullptr; - } - return lookupDispatchInvoker( - kGeneratedObjCDispatchEntries, dispatchId); -} - -inline CFunctionPreparedInvoker lookupCFunctionPreparedInvoker( - uint64_t dispatchId) { - if (!isGeneratedDispatchEnabled()) { - return nullptr; - } - return lookupDispatchInvoker( - kGeneratedCFunctionDispatchEntries, dispatchId); -} - -inline BlockPreparedInvoker lookupBlockPreparedInvoker(uint64_t dispatchId) { - if (!isGeneratedDispatchEnabled()) { - return nullptr; - } - return lookupDispatchInvoker( - kGeneratedBlockDispatchEntries, dispatchId); -} - -inline bool isPreparedGeneratedDispatchRequired() { -#if NS_HAS_GENERATED_SIGNATURE_DISPATCH && \ - (NS_GSD_BACKEND_PREPARED || NS_GSD_BACKEND_HERMES) - return isGeneratedDispatchEnabled(); -#else - return false; -#endif -} - -} // namespace nativescript +// Moved to jsi/shared/PreparedSignatureDispatch.h so the Android runtime can +// use it. This forwarding header keeps the Apple include paths unchanged. +// +// SignatureDispatchCore.h is included first because callers of this header +// have always got it transitively, and it is what computes the dispatch ids +// from the Objective-C metadata. + +#include "ffi/objc/shared/SignatureDispatchCore.h" +#include "jsi/shared/PreparedSignatureDispatch.h" #endif // NATIVESCRIPT_FFI_SHARED_PREPARED_SIGNATURE_DISPATCH_H diff --git a/NativeScript/jsi/hermes/NativeApiJsi.h b/NativeScript/jsi/hermes/NativeApiJsi.h new file mode 100644 index 000000000..a84d80bd4 --- /dev/null +++ b/NativeScript/jsi/hermes/NativeApiJsi.h @@ -0,0 +1,37 @@ +#ifndef NATIVE_API_JSI_H +#define NATIVE_API_JSI_H + +#include + +#include "jsi/shared/NativeApiBackendConfig.h" + +namespace nativescript { + +using NativeApiJsiScheduler = NativeApiBackendScheduler; +using NativeApiJsiConfig = NativeApiBackendConfig; + +facebook::jsi::Object CreateNativeApiJSI( + facebook::jsi::Runtime& runtime, + const NativeApiJsiConfig& config = NativeApiJsiConfig{}); + +void InstallNativeApiJSI( + facebook::jsi::Runtime& runtime, + const NativeApiJsiConfig& config = NativeApiJsiConfig{}); + +// Wraps an Objective-C object in the same bridge host object used by ordinary +// native crossings. `void*` keeps this contract usable from plain C++ callers. +// When `ownsObject` is true, the wrapper consumes the caller's +1 retain. +facebook::jsi::Value NativeScriptWrapNativeObject( + facebook::jsi::Runtime& runtime, void* object, bool ownsObject = false); + +// Returns the native pointer behind a live bridge wrapper, or nullptr when the +// value is not a supported native-object wrapper. +void* NativeScriptUnwrapNativeObject(facebook::jsi::Runtime& runtime, + const facebook::jsi::Value& value); + +} // namespace nativescript + +extern "C" void NativeScriptInstallNativeApiJSI( + facebook::jsi::Runtime* runtime, const char* metadataPath); + +#endif // NATIVE_API_JSI_H diff --git a/NativeScript/jsi/shared/PreparedSignatureDispatch.h b/NativeScript/jsi/shared/PreparedSignatureDispatch.h new file mode 100644 index 000000000..17cfc40a4 --- /dev/null +++ b/NativeScript/jsi/shared/PreparedSignatureDispatch.h @@ -0,0 +1,87 @@ +#ifndef NATIVESCRIPT_JSI_SHARED_PREPARED_SIGNATURE_DISPATCH_H +#define NATIVESCRIPT_JSI_SHARED_PREPARED_SIGNATURE_DISPATCH_H + +// Lookup side of generated signature dispatch: given a dispatch id, find the +// ahead-of-time generated trampoline for it. +// +// Platform-neutral. It needs only the hashing/lookup core, not the +// metadata-format-specific half that computes the ids -- which is why this can +// live here while ffi/objc/shared/SignatureDispatchCore.h cannot. + +#include "jsi/shared/SignatureHashing.h" + +#ifndef NS_GSD_BACKEND_PREPARED +#define NS_GSD_BACKEND_PREPARED 0 +#endif + +#ifndef NS_GSD_BACKEND_HERMES +#define NS_GSD_BACKEND_HERMES 0 +#endif + +#ifndef NS_GSD_BACKEND_NAPI +#define NS_GSD_BACKEND_NAPI 0 +#endif + +#ifndef NS_HAS_GENERATED_SIGNATURE_DISPATCH +#define NS_HAS_GENERATED_SIGNATURE_DISPATCH 0 +#endif + +#define NS_REQUIRES_GENERATED_SIGNATURE_DISPATCH \ + (NS_GSD_BACKEND_HERMES || NS_GSD_BACKEND_NAPI || NS_GSD_BACKEND_PREPARED) + +#if NS_REQUIRES_GENERATED_SIGNATURE_DISPATCH && \ + !NS_HAS_GENERATED_SIGNATURE_DISPATCH +#error GeneratedSignatureDispatch.inc did not enable this generated signature dispatch backend. +#endif + +#if !NS_HAS_GENERATED_SIGNATURE_DISPATCH +namespace nativescript { +inline constexpr ObjCDispatchEntry kGeneratedObjCDispatchEntries[] = { + {0, nullptr}}; +inline constexpr CFunctionDispatchEntry kGeneratedCFunctionDispatchEntries[] = { + {0, nullptr}}; +inline constexpr BlockDispatchEntry kGeneratedBlockDispatchEntries[] = { + {0, nullptr}}; +} // namespace nativescript +#endif + +namespace nativescript { + +inline ObjCPreparedInvoker lookupObjCPreparedInvoker(uint64_t dispatchId) { + if (!isGeneratedDispatchEnabled()) { + return nullptr; + } + return lookupDispatchInvoker( + kGeneratedObjCDispatchEntries, dispatchId); +} + +inline CFunctionPreparedInvoker lookupCFunctionPreparedInvoker( + uint64_t dispatchId) { + if (!isGeneratedDispatchEnabled()) { + return nullptr; + } + return lookupDispatchInvoker( + kGeneratedCFunctionDispatchEntries, dispatchId); +} + +inline BlockPreparedInvoker lookupBlockPreparedInvoker(uint64_t dispatchId) { + if (!isGeneratedDispatchEnabled()) { + return nullptr; + } + return lookupDispatchInvoker( + kGeneratedBlockDispatchEntries, dispatchId); +} + +inline bool isPreparedGeneratedDispatchRequired() { +#if NS_HAS_GENERATED_SIGNATURE_DISPATCH && \ + (NS_GSD_BACKEND_PREPARED || NS_GSD_BACKEND_HERMES) + return isGeneratedDispatchEnabled(); +#else + return false; +#endif +} + +} // namespace nativescript + +#endif // NATIVESCRIPT_JSI_SHARED_PREPARED_SIGNATURE_DISPATCH_H From 7cdeb2e1b4735fb4d1877dd3cb90fc20a6a1ccef Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Thu, 30 Jul 2026 23:25:00 +0500 Subject: [PATCH 6/7] chore(ci): teach the FFI boundary check about the shared jsi tree check_ffi_boundaries.sh forbade `#include "jsi/` in every non-Hermes backend. That was a sound proxy when the only thing a quoted jsi/ path could mean was facebook::jsi -- but NativeScript/jsi is now our own platform-neutral engine layer, which every backend is supposed to include. The rule started failing on exactly the includes this branch is built around. Narrow it to `#include "jsi/hermes/`, which preserves the actual intent: facebook::jsi and the Hermes contract header stay Hermes-only, while the shared engine layer is open to all. The facebook::jsi, &2 exit 1 @@ -164,6 +169,9 @@ fi if [ -d "$JNI_NAPI_DIR" ] && search_sources '(^|[^[:alnum:]_])(facebook::jsi|v8::|JSContextRef|JSValueRef|JSContext|JSValue|JSRuntime|quickjs)($|[^[:alnum:]_])|(&2 exit 1 fi From 42467221a724c42f2ef432cf97172f059484d19a Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Fri, 28 Aug 2026 02:07:37 -0400 Subject: [PATCH 7/7] fix(react-native): package the shared JSI headers --- packages/react-native/NativeScriptNativeApi.podspec | 1 + packages/react-native/test/runtime-callback-policy.test.js | 2 +- scripts/build_react_native_turbomodule.sh | 4 ++++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/react-native/NativeScriptNativeApi.podspec b/packages/react-native/NativeScriptNativeApi.podspec index 153208654..bfb685906 100644 --- a/packages/react-native/NativeScriptNativeApi.podspec +++ b/packages/react-native/NativeScriptNativeApi.podspec @@ -20,6 +20,7 @@ Pod::Spec.new do |s| "ios/**/*.{h,mm}", "native-api/ffi/objc/hermes/**/*.h", "native-api/ffi/objc/shared/**/*.h", + "native-api/jsi/**/*.h", "native-api/ffi/objc/hermes/NativeApiJsi.mm" ] s.exclude_files = "ios/Fabric/**/*" unless fabric_enabled diff --git a/packages/react-native/test/runtime-callback-policy.test.js b/packages/react-native/test/runtime-callback-policy.test.js index 4e058e18e..cd4c362f3 100644 --- a/packages/react-native/test/runtime-callback-policy.test.js +++ b/packages/react-native/test/runtime-callback-policy.test.js @@ -85,7 +85,7 @@ assert( ); for (const relativePath of [ - "NativeScript/ffi/objc/shared/NativeApiBackendConfig.h", + "NativeScript/jsi/shared/NativeApiBackendConfig.h", ]) { const source = readRepo(relativePath); assert( diff --git a/scripts/build_react_native_turbomodule.sh b/scripts/build_react_native_turbomodule.sh index f5d542ca1..c6a566677 100755 --- a/scripts/build_react_native_turbomodule.sh +++ b/scripts/build_react_native_turbomodule.sh @@ -69,6 +69,8 @@ mkdir -p \ "$PACKAGE_DIR/native-api/ffi/objc/shared" \ "$PACKAGE_DIR/native-api/ffi/objc/shared/bridge" \ "$PACKAGE_DIR/native-api/ffi/objc/shared/bridge/host_objects" \ + "$PACKAGE_DIR/native-api/jsi/hermes" \ + "$PACKAGE_DIR/native-api/jsi/shared" \ "$PACKAGE_DIR/native-api/metadata/include" \ "$PACKAGE_DIR/metadata" \ "$PACKAGE_DIR/ios/vendor/libffi/include" \ @@ -86,6 +88,8 @@ cp NativeScript/ffi/objc/shared/bridge/host_objects/*.mm \ cp NativeScript/ffi/objc/shared/NativeApiBackendConfig.h "$PACKAGE_DIR/native-api/ffi/objc/shared/" cp NativeScript/ffi/objc/shared/SignatureDispatchCore.h "$PACKAGE_DIR/native-api/ffi/objc/shared/" cp NativeScript/ffi/objc/shared/PreparedSignatureDispatch.h "$PACKAGE_DIR/native-api/ffi/objc/shared/" +cp NativeScript/jsi/hermes/*.h "$PACKAGE_DIR/native-api/jsi/hermes/" +cp NativeScript/jsi/shared/*.h "$PACKAGE_DIR/native-api/jsi/shared/" cp "$GENERATED_SIGNATURE_DISPATCH" "$PACKAGE_DIR/native-api/ffi/objc/shared/GeneratedSignatureDispatch.inc" GENERATED_GSD_SIGNATURE_DISPATCH="$(dirname "$GENERATED_SIGNATURE_DISPATCH")/GeneratedGsdSignatureDispatch.inc" if [ -f "$GENERATED_GSD_SIGNATURE_DISPATCH" ]; then