diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ce8f3cc0..627aeb28 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,9 @@ jobs: - name: Install Dependencies run: npm install + - name: Check FFI backend boundaries + run: npm run check:ffi-boundaries + - name: Download V8 run: ./scripts/download_v8.sh @@ -49,4 +52,6 @@ jobs: IOS_BUILD_TIMEOUT_MS: "600000" IOS_TEST_TIMEOUT_MS: "600000" IOS_TEST_INACTIVITY_TIMEOUT_MS: "180000" + IOS_TEST_VERBOSE_SPECS: "1" + IOS_SIMCTL_QUERY_TIMEOUT_MS: "10000" run: npm run test:ios diff --git a/.github/workflows/npm_trusted_release.yml b/.github/workflows/npm_trusted_release.yml index 04c54be1..39e81579 100644 --- a/.github/workflows/npm_trusted_release.yml +++ b/.github/workflows/npm_trusted_release.yml @@ -1,18 +1,20 @@ -name: NPM Trusted Release (iOS engines) +name: NPM Trusted Release -# Publishes one or more engine-specific NativeScript iOS runtime packages +# Publishes one or more NativeScript iOS npm packages # (@nativescript/ios-v8, @nativescript/ios-hermes, @nativescript/ios-jsc, -# @nativescript/ios-quickjs) via npm trusted publishing (OIDC). +# @nativescript/ios-quickjs, @nativescript/react-native) via npm trusted +# publishing (OIDC). # # Each package must be configured on npmjs.com with a trusted publisher that # points at this repository + workflow + environment. With `engine: all`, the -# workflow fans out across all four engines via a matrix. +# workflow fans out across the four iOS engine packages via a matrix; use +# `engine: react-native` to publish @nativescript/react-native. on: workflow_dispatch: inputs: engine: - description: "Engine to release (or 'all' to publish every engine)" + description: "Package to release (engine package, react-native, or 'all' for every iOS engine)" required: true type: choice default: v8 @@ -21,6 +23,7 @@ on: - hermes - jsc - quickjs + - react-native - all release-type: description: "Version bump (patch/minor/major publish to 'latest'; prerelease uses 'preid' as the dist-tag)" @@ -32,6 +35,10 @@ on: - patch - minor - major + version: + description: "Exact npm version to publish; overrides release-type/preid. Use a prerelease version for preview publishes, e.g. 9.0.0-preview.0" + required: false + type: string preid: description: "Prerelease identifier (used only when release-type=prerelease; also becomes the npm dist-tag, e.g. next | canary)" required: false @@ -44,7 +51,7 @@ on: default: true concurrency: - # Avoid overlapping publishes on the same ref/engine selection. + # Avoid overlapping publishes on the same ref/package selection. group: npm-trusted-release-${{ github.ref }}-${{ inputs.engine }} cancel-in-progress: false @@ -53,34 +60,45 @@ env: jobs: matrix: - name: Resolve engine matrix + name: Resolve package matrix runs-on: ubuntu-latest + permissions: {} outputs: - engines: ${{ steps.compute.outputs.engines }} + targets: ${{ steps.compute.outputs.targets }} steps: - name: Compute matrix id: compute + env: + ENGINE: ${{ inputs.engine }} run: | - if [ "${{ inputs.engine }}" = "all" ]; then - echo 'engines=["v8","hermes","jsc","quickjs"]' >> "$GITHUB_OUTPUT" - else - echo "engines=[\"${{ inputs.engine }}\"]" >> "$GITHUB_OUTPUT" - fi + set -euo pipefail + case "$ENGINE" in + all) + echo 'targets=["v8","hermes","jsc","quickjs"]' >> "$GITHUB_OUTPUT" + ;; + v8|hermes|jsc|quickjs|react-native) + printf 'targets=["%s"]\n' "$ENGINE" >> "$GITHUB_OUTPUT" + ;; + *) + echo "Unsupported engine: $ENGINE" >&2 + exit 1 + ;; + esac build: - name: Build ${{ matrix.engine }} + name: Build ${{ matrix.target }} needs: matrix runs-on: macos-26 + permissions: + contents: read strategy: fail-fast: false matrix: - engine: ${{ fromJson(needs.matrix.outputs.engines) }} + target: ${{ fromJson(needs.matrix.outputs.targets) }} outputs: - # Per-engine outputs aren't natively supported with matrices, so each job + # Per-target outputs aren't natively supported with matrices, so each job # uploads its computed metadata alongside the tarball artifact. placeholder: noop - env: - IOS_VARIANT: ios-${{ matrix.engine }} steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2 @@ -118,14 +136,34 @@ jobs: - name: Bump version id: bump shell: bash + env: + RELEASE_TYPE: ${{ inputs.release-type }} + PACKAGE_VERSION: ${{ inputs.version }} + PREID: ${{ inputs.preid }} + TARGET: ${{ matrix.target }} run: | set -euo pipefail - release_type='${{ inputs.release-type }}' - preid='${{ inputs.preid }}' - pkg_dir="packages/${IOS_VARIANT}" + release_type="$RELEASE_TYPE" + package_version="$PACKAGE_VERSION" + preid="$PREID" + target="$TARGET" + if [ "$target" = "react-native" ]; then + pkg_dir="packages/react-native" + package_name="@nativescript/react-native" + tarball_basename="nativescript-react-native" + npm_tag_target="react-native" + else + pkg_dir="packages/ios-${target}" + package_name="@nativescript/ios-${target}" + tarball_basename="nativescript-ios-${target}" + npm_tag_target="ios-${target}" + echo "IOS_VARIANT=ios-${target}" >> "$GITHUB_ENV" + fi pushd "$pkg_dir" >/dev/null - if [ "$release_type" = "prerelease" ]; then + if [ -n "$package_version" ]; then + npm version "$package_version" --no-git-tag-version >/dev/null + elif [ "$release_type" = "prerelease" ]; then npm version prerelease --preid "$preid" --no-git-tag-version >/dev/null else npm version "$release_type" --no-git-tag-version >/dev/null @@ -133,40 +171,67 @@ jobs: NPM_VERSION=$(node -e "console.log(require('./package.json').version)") popd >/dev/null - NPM_TAG=$(NPM_VERSION="$NPM_VERSION" node ./scripts/get-npm-tag.js "$IOS_VARIANT") + NPM_TAG=$(NPM_VERSION="$NPM_VERSION" node ./scripts/get-npm-tag.js "$npm_tag_target") + if [ -n "$package_version" ] && [ "$release_type" = "prerelease" ] && [ "$NPM_TAG" = "latest" ]; then + echo "Exact prerelease publishes must include a prerelease identifier (for example 9.0.0-preview.0)." >&2 + exit 1 + fi echo "NPM_VERSION=$NPM_VERSION" >> "$GITHUB_OUTPUT" echo "NPM_TAG=$NPM_TAG" >> "$GITHUB_OUTPUT" - echo "Resolved $IOS_VARIANT@$NPM_VERSION (tag: $NPM_TAG)" - - name: Build (--${{ matrix.engine }}) - run: ./scripts/build_all_ios.sh --${{ matrix.engine }} + echo "PACKAGE_DIR=$pkg_dir" >> "$GITHUB_OUTPUT" + echo "PACKAGE_NAME=$package_name" >> "$GITHUB_OUTPUT" + echo "TARBALL_BASENAME=$tarball_basename" >> "$GITHUB_OUTPUT" + echo "Resolved $package_name@$NPM_VERSION (tag: $NPM_TAG)" + - name: Build iOS engine (--${{ matrix.target }}) + if: ${{ matrix.target != 'react-native' }} + env: + TARGET: ${{ matrix.target }} + run: ./scripts/build_all_ios.sh "--${TARGET}" + - name: Build @nativescript/react-native + if: ${{ matrix.target == 'react-native' }} + run: | + ./scripts/build_all_react_native.sh + ./scripts/build_react_native_turbomodule.sh - name: Record metadata shell: bash + env: + TARGET: ${{ matrix.target }} + PACKAGE_DIR: ${{ steps.bump.outputs.PACKAGE_DIR }} + PACKAGE_NAME: ${{ steps.bump.outputs.PACKAGE_NAME }} + NPM_VERSION: ${{ steps.bump.outputs.NPM_VERSION }} + NPM_TAG: ${{ steps.bump.outputs.NPM_TAG }} + TARBALL_BASENAME: ${{ steps.bump.outputs.TARBALL_BASENAME }} run: | - mkdir -p packages/${IOS_VARIANT}/dist - cat > packages/${IOS_VARIANT}/dist/release-meta.json < "$package_dir/dist/release-meta.json" <&2 exit 1 @@ -212,9 +279,11 @@ jobs: NPM_VERSION=$(node -e "console.log(require('./$meta').version)") NPM_TAG=$(node -e "console.log(require('./$meta').tag)") PACKAGE_NAME=$(node -e "console.log(require('./$meta').package_name)") + TARBALL=$(node -e "console.log(require('./$meta').tarball)") echo "NPM_VERSION=$NPM_VERSION" >> "$GITHUB_OUTPUT" echo "NPM_TAG=$NPM_TAG" >> "$GITHUB_OUTPUT" echo "PACKAGE_NAME=$PACKAGE_NAME" >> "$GITHUB_OUTPUT" + echo "TARBALL=$TARBALL" >> "$GITHUB_OUTPUT" - name: Publish package (OIDC trusted publishing) if: ${{ vars.USE_NPM_TOKEN != 'true' }} shell: bash @@ -222,12 +291,14 @@ jobs: NPM_VERSION: ${{ steps.meta.outputs.NPM_VERSION }} NPM_TAG: ${{ steps.meta.outputs.NPM_TAG }} PACKAGE_NAME: ${{ steps.meta.outputs.PACKAGE_NAME }} + TARBALL: ${{ steps.meta.outputs.TARBALL }} + TARGET: ${{ matrix.target }} DRY_RUN: ${{ inputs.dry-run }} NODE_AUTH_TOKEN: "" run: | set -euo pipefail - TARBALL="packages/ios-${{ matrix.engine }}/dist/nativescript-ios-${{ matrix.engine }}-${NPM_VERSION}.tgz" - PUBLISH_ARGS=("$TARBALL" --tag "$NPM_TAG" --access public --provenance) + TARBALL_PATH="npm-package/${TARGET}/${TARBALL}" + PUBLISH_ARGS=("$TARBALL_PATH" --tag "$NPM_TAG" --access public --provenance) if [ "$DRY_RUN" = "true" ]; then PUBLISH_ARGS+=(--dry-run) fi @@ -245,12 +316,14 @@ jobs: NPM_VERSION: ${{ steps.meta.outputs.NPM_VERSION }} NPM_TAG: ${{ steps.meta.outputs.NPM_TAG }} PACKAGE_NAME: ${{ steps.meta.outputs.PACKAGE_NAME }} + TARBALL: ${{ steps.meta.outputs.TARBALL }} + TARGET: ${{ matrix.target }} DRY_RUN: ${{ inputs.dry-run }} NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} run: | set -euo pipefail - TARBALL="packages/ios-${{ matrix.engine }}/dist/nativescript-ios-${{ matrix.engine }}-${NPM_VERSION}.tgz" - PUBLISH_ARGS=("$TARBALL" --tag "$NPM_TAG" --access public --provenance) + TARBALL_PATH="npm-package/${TARGET}/${TARBALL}" + PUBLISH_ARGS=("$TARBALL_PATH" --tag "$NPM_TAG" --access public --provenance) if [ "$DRY_RUN" = "true" ]; then PUBLISH_ARGS+=(--dry-run) fi @@ -265,13 +338,24 @@ jobs: - build - publish runs-on: ubuntu-latest + permissions: {} steps: - name: Print summary + env: + PACKAGE_SELECTION: ${{ inputs.engine }} + RELEASE_TYPE: ${{ inputs.release-type }} + PACKAGE_VERSION: ${{ inputs.version }} + PREID: ${{ inputs.preid }} + DRY_RUN: ${{ inputs.dry-run }} + TARGETS: ${{ needs.matrix.outputs.targets }} + BUILD_RESULT: ${{ needs.build.result }} + PUBLISH_RESULT: ${{ needs.publish.result }} run: | - echo "Engine selection: ${{ inputs.engine }}" - echo "Release type: ${{ inputs.release-type }}" - echo "Preid: ${{ inputs.preid }}" - echo "Dry run: ${{ inputs.dry-run }}" - echo "Engines: ${{ needs.matrix.outputs.engines }}" - echo "Build result: ${{ needs.build.result }}" - echo "Publish result: ${{ needs.publish.result }}" + echo "Package selection: $PACKAGE_SELECTION" + echo "Release type: $RELEASE_TYPE" + echo "Exact version: $PACKAGE_VERSION" + echo "Preid: $PREID" + echo "Dry run: $DRY_RUN" + echo "Targets: $TARGETS" + echo "Build result: $BUILD_RESULT" + echo "Publish result: $PUBLISH_RESULT" diff --git a/.gitignore b/.gitignore index dc351670..edc6abe5 100644 --- a/.gitignore +++ b/.gitignore @@ -65,4 +65,11 @@ packages/*/types SwiftBindgen # Generated Objective-C/C dispatch wrappers -NativeScript/ffi/GeneratedSignatureDispatch.inc +NativeScript/ffi/napi/GeneratedSignatureDispatch.inc +NativeScript/ffi/napi/GeneratedSignatureDispatch.inc.stamp + +# React Native TurboModule package staging +packages/react-native/dist/ +packages/react-native/ios/vendor/ +packages/react-native/metadata/ +packages/react-native/native-api-jsi/ diff --git a/NativeScript/CMakeLists.txt b/NativeScript/CMakeLists.txt index 418e6cab..b9386f46 100644 --- a/NativeScript/CMakeLists.txt +++ b/NativeScript/CMakeLists.txt @@ -20,9 +20,13 @@ set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${COMMON_FLAGS}") # Arguments set(TARGET_PLATFORM "macos" CACHE STRING "Target platform for the Objective-C bridge") set(TARGET_ENGINE "v8" CACHE STRING "Target JS engine for the NativeScript runtime") +set(NS_FFI_BACKEND "auto" CACHE STRING "FFI backend: auto, napi, or direct") +set(NS_GSD_BACKEND "auto" CACHE STRING "Generated signature dispatch backend: auto, v8, jsc, quickjs, hermes, napi, or none") set(METADATA_SIZE 0 CACHE STRING "Size of embedded metadata in bytes") set(BUILD_CLI_BINARY OFF CACHE BOOL "Build the NativeScript CLI binary") set(BUILD_MACOS_NODE_API OFF CACHE BOOL "Build the NativeScript macOS Node API dylib") +set_property(CACHE NS_FFI_BACKEND PROPERTY STRINGS auto napi direct) +set_property(CACHE NS_GSD_BACKEND PROPERTY STRINGS auto v8 jsc quickjs hermes napi none) if (BUILD_MACOS_NODE_API) set(BUILD_FRAMEWORK OFF) @@ -132,39 +136,154 @@ message(STATUS "TARGET_ENGINE = ${TARGET_ENGINE}") message(STATUS "ENABLE_JS_RUNTIME = ${ENABLE_JS_RUNTIME}") message(STATUS "GENERIC_NAPI = ${GENERIC_NAPI}") +if(NS_FFI_BACKEND STREQUAL "auto") + if(GENERIC_NAPI OR TARGET_ENGINE_NONE) + set(NS_EFFECTIVE_FFI_BACKEND "napi") + elseif(TARGET_ENGINE_HERMES OR TARGET_ENGINE_V8 OR TARGET_ENGINE_JSC OR TARGET_ENGINE_QUICKJS) + set(NS_EFFECTIVE_FFI_BACKEND "direct") + else() + set(NS_EFFECTIVE_FFI_BACKEND "napi") + endif() +elseif(NS_FFI_BACKEND STREQUAL "napi" OR NS_FFI_BACKEND STREQUAL "direct") + set(NS_EFFECTIVE_FFI_BACKEND "${NS_FFI_BACKEND}") +else() + message(FATAL_ERROR "Unknown NS_FFI_BACKEND: ${NS_FFI_BACKEND}") +endif() + +if(NS_EFFECTIVE_FFI_BACKEND STREQUAL "direct" AND + (GENERIC_NAPI OR TARGET_ENGINE_NONE OR BUILD_MACOS_NODE_API)) + message(FATAL_ERROR "NS_FFI_BACKEND=direct requires an embedded JS runtime build") +endif() + +message(STATUS "NS_FFI_BACKEND = ${NS_FFI_BACKEND} (${NS_EFFECTIVE_FFI_BACKEND})") + +if(NS_EFFECTIVE_FFI_BACKEND STREQUAL "direct" AND + NOT (NS_GSD_BACKEND STREQUAL "auto" OR NS_GSD_BACKEND STREQUAL "none")) + message(FATAL_ERROR + "NS_GSD_BACKEND is only used by the Node-API FFI backend. " + "Use NS_GSD_BACKEND=auto or none with NS_FFI_BACKEND=direct.") +endif() + +if(NS_GSD_BACKEND STREQUAL "auto") + if(NS_EFFECTIVE_FFI_BACKEND STREQUAL "direct") + set(NS_EFFECTIVE_GSD_BACKEND "none") + else() + set(NS_EFFECTIVE_GSD_BACKEND "napi") + endif() +elseif(NS_GSD_BACKEND STREQUAL "v8" OR + NS_GSD_BACKEND STREQUAL "jsc" OR + NS_GSD_BACKEND STREQUAL "quickjs" OR + NS_GSD_BACKEND STREQUAL "hermes" OR + NS_GSD_BACKEND STREQUAL "napi" OR + NS_GSD_BACKEND STREQUAL "none") + set(NS_EFFECTIVE_GSD_BACKEND "${NS_GSD_BACKEND}") +else() + message(FATAL_ERROR "Unknown NS_GSD_BACKEND: ${NS_GSD_BACKEND}") +endif() + +if(NS_EFFECTIVE_GSD_BACKEND STREQUAL "v8" AND NOT TARGET_ENGINE_V8) + message(FATAL_ERROR "NS_GSD_BACKEND=v8 requires TARGET_ENGINE=v8") +endif() +if(NS_EFFECTIVE_GSD_BACKEND STREQUAL "jsc" AND NOT TARGET_ENGINE_JSC) + message(FATAL_ERROR "NS_GSD_BACKEND=jsc requires TARGET_ENGINE=jsc") +endif() +if(NS_EFFECTIVE_GSD_BACKEND STREQUAL "quickjs" AND NOT TARGET_ENGINE_QUICKJS) + message(FATAL_ERROR "NS_GSD_BACKEND=quickjs requires TARGET_ENGINE=quickjs") +endif() +if(NS_EFFECTIVE_GSD_BACKEND STREQUAL "hermes" AND NOT TARGET_ENGINE_HERMES) + message(FATAL_ERROR "NS_GSD_BACKEND=hermes requires TARGET_ENGINE=hermes") +endif() +if(NS_EFFECTIVE_FFI_BACKEND STREQUAL "napi" AND + NOT (NS_EFFECTIVE_GSD_BACKEND STREQUAL "napi" OR + NS_EFFECTIVE_GSD_BACKEND STREQUAL "none")) + message(FATAL_ERROR + "NS_FFI_BACKEND=napi is the pure Node-API FFI backend and only supports " + "NS_GSD_BACKEND=napi or none.") +endif() + +message(STATUS "NS_GSD_BACKEND = ${NS_GSD_BACKEND} (${NS_EFFECTIVE_GSD_BACKEND})") + # Set up sources include_directories( ./ + ffi/shared ../metadata-generator/include napi/common libffi/${LIBFFI_BUILD}/include ) +set(FFI_SHARED_SOURCE_FILES + ffi/shared/Tasks.cpp +) + +set(FFI_NAPI_SOURCE_FILES + ffi/napi/AutoreleasePool.mm + ffi/napi/Protocol.mm + ffi/napi/ObjCBridge.mm + ffi/napi/Block.mm + ffi/napi/Class.mm + ffi/napi/Closure.mm + ffi/napi/ClassMember.mm + ffi/napi/Cif.mm + ffi/napi/TypeConv.mm + ffi/napi/Util.mm + ffi/napi/Struct.mm + ffi/napi/ObjectRef.mm + ffi/napi/JSObject.mm + ffi/napi/Enum.mm + ffi/napi/Variable.mm + ffi/napi/Object.mm + ffi/napi/CFunction.mm + ffi/napi/Interop.mm + ffi/napi/InlineFunctions.mm + ffi/napi/ClassBuilder.mm +) + +set(FFI_DIRECT_SHARED_SOURCE_FILES + ffi/shared/direct/EmbeddedMetadata.mm +) + +set(FFI_HERMES_DIRECT_SOURCE_FILES + ${FFI_DIRECT_SHARED_SOURCE_FILES} + ffi/hermes/jsi/NativeApiJsi.mm +) + +set(FFI_V8_DIRECT_SOURCE_FILES + ${FFI_DIRECT_SHARED_SOURCE_FILES} + ffi/v8/NativeApiV8.mm + ffi/v8/NativeApiV8HostObjects.mm + ffi/v8/NativeApiV8Runtime.mm + ffi/v8/NativeApiV8Value.mm +) + +set(FFI_JSC_DIRECT_SOURCE_FILES + ${FFI_DIRECT_SHARED_SOURCE_FILES} + ffi/jsc/NativeApiJSC.mm + ffi/jsc/NativeApiJSCHostObjects.mm + ffi/jsc/NativeApiJSCRuntime.mm + ffi/jsc/NativeApiJSCValue.mm +) + +set(FFI_QUICKJS_DIRECT_SOURCE_FILES + ${FFI_DIRECT_SHARED_SOURCE_FILES} + ffi/quickjs/NativeApiQuickJSHostObjects.mm + ffi/quickjs/NativeApiQuickJS.mm + ffi/quickjs/NativeApiQuickJSRuntime.mm + ffi/quickjs/NativeApiQuickJSValue.mm +) + set(SOURCE_FILES - ffi/AutoreleasePool.mm - ffi/Protocol.mm - ffi/ObjCBridge.mm - ffi/Block.mm - ffi/Class.mm - ffi/Closure.mm - ffi/ClassMember.mm - ffi/Cif.mm - ffi/TypeConv.mm - ffi/Util.mm - ffi/Struct.mm - ffi/ObjectRef.mm - ffi/JSObject.mm - ffi/Enum.mm - ffi/Variable.mm - ffi/Object.mm - ffi/CFunction.mm - ffi/Interop.mm - ffi/InlineFunctions.mm - ffi/ClassBuilder.mm - ffi/NativeScriptException.mm - ffi/Tasks.cpp + ${FFI_SHARED_SOURCE_FILES} + runtime/NativeScriptException.mm ) +if(NS_EFFECTIVE_FFI_BACKEND STREQUAL "napi") + set(SOURCE_FILES + ${SOURCE_FILES} + ${FFI_NAPI_SOURCE_FILES} + ) +endif() + if(ENABLE_JS_RUNTIME) set(SOURCE_FILES ${SOURCE_FILES} @@ -209,6 +328,13 @@ if(ENABLE_JS_RUNTIME) napi/v8/SimpleAllocator.cpp ) + if(NS_EFFECTIVE_FFI_BACKEND STREQUAL "direct") + set(SOURCE_FILES + ${SOURCE_FILES} + ${FFI_V8_DIRECT_SOURCE_FILES} + ) + endif() + elseif(TARGET_ENGINE_HERMES) set(HERMES_HEADERS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../Frameworks/hermes-headers") @@ -233,6 +359,13 @@ if(ENABLE_JS_RUNTIME) napi/hermes/jsr.cpp ) + if(NS_EFFECTIVE_FFI_BACKEND STREQUAL "direct") + set(SOURCE_FILES + ${SOURCE_FILES} + ${FFI_HERMES_DIRECT_SOURCE_FILES} + ) + endif() + elseif(TARGET_ENGINE_QUICKJS) set(MI_BUILD_OBJECT OFF) set(MI_OVERRIDE OFF) @@ -265,6 +398,13 @@ if(ENABLE_JS_RUNTIME) napi/quickjs/jsr.cpp ) + if(NS_EFFECTIVE_FFI_BACKEND STREQUAL "direct") + set(SOURCE_FILES + ${SOURCE_FILES} + ${FFI_QUICKJS_DIRECT_SOURCE_FILES} + ) + endif() + elseif(TARGET_ENGINE_JSC) include_directories( napi/jsc @@ -276,6 +416,14 @@ if(ENABLE_JS_RUNTIME) napi/jsc/jsc-api.cpp napi/jsc/jsr.cpp ) + + if(NS_EFFECTIVE_FFI_BACKEND STREQUAL "direct") + set(SOURCE_FILES + ${SOURCE_FILES} + ${FFI_JSC_DIRECT_SOURCE_FILES} + ) + endif() + endif() else() include_directories( @@ -345,6 +493,60 @@ if(TARGET_ENGINE_V8 AND TARGET_PLATFORM_IOS) ) endif() +if(ENABLE_JS_RUNTIME) + target_compile_definitions(${NAME} PRIVATE ENABLE_JS_RUNTIME) +endif() + +if(TARGET_PLATFORM_MACOS) + target_compile_definitions(${NAME} PRIVATE TARGET_PLATFORM_MACOS) +endif() + +if(TARGET_ENGINE_HERMES) + target_compile_definitions(${NAME} PRIVATE TARGET_ENGINE_HERMES) +elseif(TARGET_ENGINE_V8) + target_compile_definitions(${NAME} PRIVATE TARGET_ENGINE_V8) +elseif(TARGET_ENGINE_QUICKJS) + target_compile_definitions(${NAME} PRIVATE TARGET_ENGINE_QUICKJS) +elseif(TARGET_ENGINE_JSC) + target_compile_definitions(${NAME} PRIVATE TARGET_ENGINE_JSC) +endif() + +set(NS_GSD_BACKEND_V8_VALUE 0) +set(NS_GSD_BACKEND_JSC_VALUE 0) +set(NS_GSD_BACKEND_QUICKJS_VALUE 0) +set(NS_GSD_BACKEND_HERMES_VALUE 0) +set(NS_GSD_BACKEND_NAPI_VALUE 0) +set(NS_FFI_BACKEND_DIRECT_VALUE 0) +set(NS_FFI_BACKEND_NAPI_VALUE 0) + +if(NS_EFFECTIVE_GSD_BACKEND STREQUAL "v8") + set(NS_GSD_BACKEND_V8_VALUE 1) +elseif(NS_EFFECTIVE_GSD_BACKEND STREQUAL "jsc") + set(NS_GSD_BACKEND_JSC_VALUE 1) +elseif(NS_EFFECTIVE_GSD_BACKEND STREQUAL "quickjs") + set(NS_GSD_BACKEND_QUICKJS_VALUE 1) +elseif(NS_EFFECTIVE_GSD_BACKEND STREQUAL "hermes") + set(NS_GSD_BACKEND_HERMES_VALUE 1) +elseif(NS_EFFECTIVE_GSD_BACKEND STREQUAL "napi") + set(NS_GSD_BACKEND_NAPI_VALUE 1) +endif() + +if(NS_EFFECTIVE_FFI_BACKEND STREQUAL "direct") + set(NS_FFI_BACKEND_DIRECT_VALUE 1) +elseif(NS_EFFECTIVE_FFI_BACKEND STREQUAL "napi") + set(NS_FFI_BACKEND_NAPI_VALUE 1) +endif() + +target_compile_definitions(${NAME} PRIVATE + NS_GSD_BACKEND_V8=${NS_GSD_BACKEND_V8_VALUE} + NS_GSD_BACKEND_JSC=${NS_GSD_BACKEND_JSC_VALUE} + NS_GSD_BACKEND_QUICKJS=${NS_GSD_BACKEND_QUICKJS_VALUE} + NS_GSD_BACKEND_HERMES=${NS_GSD_BACKEND_HERMES_VALUE} + NS_GSD_BACKEND_NAPI=${NS_GSD_BACKEND_NAPI_VALUE} + NS_FFI_BACKEND_DIRECT=${NS_FFI_BACKEND_DIRECT_VALUE} + NS_FFI_BACKEND_NAPI=${NS_FFI_BACKEND_NAPI_VALUE} +) + set(FRAMEWORK_VERSION_VALUE "${VERSION}") if(TARGET_PLATFORM_MACOS) # macOS framework consumers (including Xcode's copy/sign phases) expect diff --git a/NativeScript/NativeScript-Prefix.pch b/NativeScript/NativeScript-Prefix.pch index e84500d5..9f55361f 100644 --- a/NativeScript/NativeScript-Prefix.pch +++ b/NativeScript/NativeScript-Prefix.pch @@ -1,6 +1,6 @@ #ifndef NativeScript_Prefix_pch #define NativeScript_Prefix_pch -#define NATIVESCRIPT_VERSION "9.0.0-napi-v8.0" +#define NATIVESCRIPT_VERSION "0.0.1" #endif /* NativeScript_Prefix_pch */ diff --git a/NativeScript/cli/main.cpp b/NativeScript/cli/main.cpp index 5f7a6c35..f9ea139c 100644 --- a/NativeScript/cli/main.cpp +++ b/NativeScript/cli/main.cpp @@ -5,12 +5,12 @@ #include #include -#include "ffi/NativeScriptException.h" +#include "runtime/NativeScriptException.h" #include "runtime/Bundle.h" #include "runtime/Runtime.h" #include "runtime/RuntimeConfig.h" #include "segappend.h" -#include "ffi/Tasks.h" +#include "ffi/shared/Tasks.h" #include "BundleLoader.h" using namespace nativescript; diff --git a/NativeScript/ffi/hermes/jsi/NativeApiJsi.h b/NativeScript/ffi/hermes/jsi/NativeApiJsi.h new file mode 100644 index 00000000..ccc2ab63 --- /dev/null +++ b/NativeScript/ffi/hermes/jsi/NativeApiJsi.h @@ -0,0 +1,43 @@ +#ifndef NATIVE_API_JSI_H +#define NATIVE_API_JSI_H + +#include +#include +#include + +#include + +namespace nativescript { + +class NativeApiJsiScheduler { + public: + virtual ~NativeApiJsiScheduler() = default; + virtual void invokeOnJS(std::function task) = 0; + virtual void invokeOnUI(std::function task) = 0; +}; + +struct NativeApiJsiConfig { + 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)> jsThreadCallbackInvoker = nullptr; + bool installGlobalSymbols = false; +}; + +facebook::jsi::Object CreateNativeApiJSI( + facebook::jsi::Runtime& runtime, + const NativeApiJsiConfig& config = NativeApiJsiConfig{}); + +void InstallNativeApiJSI( + facebook::jsi::Runtime& runtime, + const NativeApiJsiConfig& config = NativeApiJsiConfig{}); + +} // namespace nativescript + +extern "C" void NativeScriptInstallNativeApiJSI( + facebook::jsi::Runtime* runtime, const char* metadataPath); + +#endif // NATIVE_API_JSI_H diff --git a/NativeScript/ffi/hermes/jsi/NativeApiJsi.mm b/NativeScript/ffi/hermes/jsi/NativeApiJsi.mm new file mode 100644 index 00000000..70a80bbf --- /dev/null +++ b/NativeScript/ffi/hermes/jsi/NativeApiJsi.mm @@ -0,0 +1,89 @@ +#include "NativeApiJsi.h" + +#ifdef TARGET_ENGINE_HERMES + +#import +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Metadata.h" +#include "MetadataReader.h" +#include "ffi.h" + +@protocol NativeApiJsiClassBuilderProtocol +@end + +#ifdef EMBED_METADATA_SIZE +extern const unsigned char embedded_metadata[EMBED_METADATA_SIZE]; +#endif + +namespace nativescript { +namespace { + +using facebook::jsi::Array; +using facebook::jsi::ArrayBuffer; +using facebook::jsi::BigInt; +using facebook::jsi::Function; +using facebook::jsi::HostObject; +using facebook::jsi::MutableBuffer; +using facebook::jsi::Object; +using facebook::jsi::PropNameID; +using facebook::jsi::Runtime; +using facebook::jsi::String; +using facebook::jsi::StringBuffer; +using facebook::jsi::Value; +using metagen::MDMemberFlag; +using metagen::MDMetadataReader; +using metagen::MDSectionOffset; +using metagen::MDTypeKind; + +// clang-format off +#include "jsi/NativeApiJsiBridge.h" +#include "jsi/NativeApiJsiHostObjects.h" +#include "jsi/NativeApiJsiCallbacks.h" +#include "jsi/NativeApiJsiConversion.h" +#include "jsi/NativeApiJsiInvocation.h" +#include "jsi/NativeApiJsiClassBuilder.h" +#include "jsi/NativeApiJsiHostObject.h" +// clang-format on + +} // namespace + +#include "jsi/NativeApiJsiInstall.h" + +} // namespace nativescript + +extern "C" void NativeScriptInstallNativeApiJSI(facebook::jsi::Runtime* runtime, + const char* metadataPath) { + if (runtime == nullptr) { + return; + } + nativescript::NativeApiJsiConfig config; + config.metadataPath = metadataPath; + nativescript::InstallNativeApiJSI(*runtime, config); +} + +#endif // TARGET_ENGINE_HERMES diff --git a/NativeScript/ffi/hermes/jsi/NativeApiJsiReactNative.h b/NativeScript/ffi/hermes/jsi/NativeApiJsiReactNative.h new file mode 100644 index 00000000..fe315d60 --- /dev/null +++ b/NativeScript/ffi/hermes/jsi/NativeApiJsiReactNative.h @@ -0,0 +1,88 @@ +#ifndef NATIVE_API_JSI_REACT_NATIVE_H +#define NATIVE_API_JSI_REACT_NATIVE_H + +#include "NativeApiJsi.h" + +#if __has_include() +#include +#define NATIVESCRIPT_HAS_REACT_NATIVE_CALL_INVOKER 1 +#elif __has_include() +#include +#define NATIVESCRIPT_HAS_REACT_NATIVE_CALL_INVOKER 1 +#else +#define NATIVESCRIPT_HAS_REACT_NATIVE_CALL_INVOKER 0 +#endif + +#if NATIVESCRIPT_HAS_REACT_NATIVE_CALL_INVOKER + +#include +#include + +namespace nativescript { + +class ReactNativeCallInvokerScheduler final : public NativeApiJsiScheduler { + public: + ReactNativeCallInvokerScheduler( + std::shared_ptr jsInvoker, + std::shared_ptr uiInvoker) + : jsInvoker_(std::move(jsInvoker)), uiInvoker_(std::move(uiInvoker)) { + if (!jsInvoker_) { + throw std::invalid_argument( + "NativeScript React Native JSI scheduler requires a JS CallInvoker"); + } + } + + void invokeOnJS(std::function task) override { + jsInvoker_->invokeAsync(std::move(task)); + } + + void invokeOnUI(std::function task) override { + if (uiInvoker_) { + uiInvoker_->invokeAsync(std::move(task)); + return; + } + auto heapTask = std::make_shared>(std::move(task)); + dispatch_async(dispatch_get_main_queue(), ^{ + (*heapTask)(); + }); + } + + private: + std::shared_ptr jsInvoker_; + std::shared_ptr uiInvoker_; +}; + +inline NativeApiJsiConfig MakeReactNativeNativeApiJsiConfig( + std::shared_ptr jsInvoker, + std::shared_ptr uiInvoker, + const char* metadataPath = nullptr, + const void* metadataPtr = nullptr, + const char* globalName = "__nativeScriptNativeApi") { + NativeApiJsiConfig config; + config.metadataPath = metadataPath; + config.metadataPtr = metadataPtr; + config.globalName = globalName; + config.installGlobalSymbols = true; + config.scheduler = std::make_shared( + std::move(jsInvoker), std::move(uiInvoker)); + return config; +} + +inline void InstallReactNativeNativeApiJSI( + facebook::jsi::Runtime& runtime, + std::shared_ptr jsInvoker, + std::shared_ptr uiInvoker, + const char* metadataPath = nullptr, + const void* metadataPtr = nullptr, + const char* globalName = "__nativeScriptNativeApi") { + InstallNativeApiJSI( + runtime, + MakeReactNativeNativeApiJsiConfig(std::move(jsInvoker), std::move(uiInvoker), + metadataPath, metadataPtr, globalName)); +} + +} // namespace nativescript + +#endif // NATIVESCRIPT_HAS_REACT_NATIVE_CALL_INVOKER + +#endif // NATIVE_API_JSI_REACT_NATIVE_H diff --git a/NativeScript/ffi/hermes/jsi/README.md b/NativeScript/ffi/hermes/jsi/README.md new file mode 100644 index 00000000..ca07222b --- /dev/null +++ b/NativeScript/ffi/hermes/jsi/README.md @@ -0,0 +1,62 @@ +# Native API JSI bridge + +This directory contains the Hermes-first JSI entrypoint for NativeScript Native +API access. + +The backend is split by FFI responsibility: + +- `../../shared/jsi/NativeApiJsiBridge.h` owns metadata indexing, symbol lookup, scheduler + state, and bridge lifetime caches. +- `../../shared/jsi/NativeApiJsiHostObjects.h` owns class, object, protocol, pointer, + reference, struct, and union host objects. +- `../../shared/jsi/NativeApiJsiCallbacks.h` owns signatures, libffi callback trampolines, + JS blocks, and native function pointer callback lifetime. +- `../../shared/jsi/NativeApiJsiConversion.h` owns JSI/native type conversion and the + `interop` helper surface. +- `../../shared/jsi/NativeApiJsiInvocation.h` owns constants, enums, C function calls, + function pointer calls, and Objective-C selector dispatch. +- `../../shared/jsi/NativeApiJsiHostObject.h` owns the public API host object exposed to JS. +- `../../shared/jsi/NativeApiJsiInstall.h` owns runtime/global installation. + +The core installer is engine-host agnostic: + +```cpp +nativescript::NativeApiJsiConfig config; +config.metadataPath = metadataPath; +config.metadataPtr = metadataPtr; +nativescript::InstallNativeApiJSI(runtime, config); +``` + +NativeScript's Hermes runtime installs this automatically as +`globalThis.__nativeScriptNativeApi`. + +React Native integrations should include `NativeApiJsiReactNative.h` from a +TurboModule implementation and pass the module's JS/UI `CallInvoker`s: + +```cpp +nativescript::InstallReactNativeNativeApiJSI( + runtime, jsInvoker, uiInvoker, metadataPath, metadataPtr); +``` + +The React Native adapter is intentionally only a scheduler/config shim. The +native API host object, metadata loading, primitive C function dispatch, +Objective-C class/object handles, and selector invocation live in the shared +JSI implementation so they can be used by both NativeScript Hermes and a React +Native TurboModule without going through Node-API. + +The direct JSI backend is still moving toward full NativeScript bridge parity. +It covers the metadata-backed Objective-C class/function/constant/enum paths +needed by the React Native TurboModule, plus metadata-backed structs/unions, +primitive array/vector value marshalling, JS blocks, C function pointer +callbacks, protocol wrappers, pointer/reference helpers, and the core `interop` +helpers (`Pointer`, `Reference`, `sizeof`, `alloc`, `free`, `adopt`, +`handleof`, `stringFromCString`, `bufferFromData`, and `addProtocol`). Struct +and union constructors, plus protocol symbols, are installed on `globalThis` +along with `interop` so common NativeScript-style calls such as +`CGRect({ origin, size })`, `interop.sizeof(CGRect)`, and +`interop.handleof(value)` work through JSI. + +The remaining RN FFI-suite skip is the explicit `interop.addMethod` decorator +hook. JavaScript-defined Objective-C subclasses created through `.extend(...)` +use the JSI class-builder path and are covered by the React Native compatibility +suite. diff --git a/NativeScript/ffi/jsc/NativeApiJSC.h b/NativeScript/ffi/jsc/NativeApiJSC.h new file mode 100644 index 00000000..0bf60c96 --- /dev/null +++ b/NativeScript/ffi/jsc/NativeApiJSC.h @@ -0,0 +1,19 @@ +#ifndef NATIVESCRIPT_FFI_JSC_NATIVE_API_JSC_H +#define NATIVESCRIPT_FFI_JSC_NATIVE_API_JSC_H + +#include "ffi/shared/direct/NativeApiDirect.h" +#include + +namespace nativescript { + +using NativeApiJSCConfig = NativeApiDirectConfig; + +void InstallNativeApiJSC(JSGlobalContextRef context, + const NativeApiJSCConfig& config = NativeApiJSCConfig{}); + +} // namespace nativescript + +extern "C" void NativeScriptInstallNativeApiJSC(JSGlobalContextRef context, + const char* metadataPath); + +#endif // NATIVESCRIPT_FFI_JSC_NATIVE_API_JSC_H diff --git a/NativeScript/ffi/jsc/NativeApiJSC.mm b/NativeScript/ffi/jsc/NativeApiJSC.mm new file mode 100644 index 00000000..b1a6fa39 --- /dev/null +++ b/NativeScript/ffi/jsc/NativeApiJSC.mm @@ -0,0 +1,70 @@ +#include "NativeApiJSC.h" + +#ifdef TARGET_ENGINE_JSC + +#include "NativeApiJSCRuntime.h" + +namespace nativescript { + +using NativeApiJsiConfig = NativeApiDirectConfig; +using NativeApiJsiScheduler = NativeApiDirectScheduler; + +namespace { + +using facebook::jsi::Array; +using facebook::jsi::ArrayBuffer; +using facebook::jsi::BigInt; +using facebook::jsi::Function; +using facebook::jsi::HostObject; +using facebook::jsi::MutableBuffer; +using facebook::jsi::Object; +using facebook::jsi::PropNameID; +using facebook::jsi::Runtime; +using facebook::jsi::String; +using facebook::jsi::StringBuffer; +using facebook::jsi::Value; +using metagen::MDMemberFlag; +using metagen::MDMetadataReader; +using metagen::MDSectionOffset; +using metagen::MDTypeKind; + +// clang-format off +#include "jsi/NativeApiJsiBridge.h" +#include "jsi/NativeApiJsiHostObjects.h" +// clang-format on +#define NATIVESCRIPT_NATIVE_API_RETAIN_RUNTIME 1 + +std::shared_ptr retainNativeApiJsiRuntime(Runtime& runtime) { + return std::make_shared(runtime.state()); +} + +// clang-format off +#include "jsi/NativeApiJsiCallbacks.h" +#include "jsi/NativeApiJsiConversion.h" +#include "jsi/NativeApiJsiInvocation.h" +#include "jsi/NativeApiJsiClassBuilder.h" +#include "jsi/NativeApiJsiHostObject.h" +// clang-format on + +} // namespace + +#include "jsi/NativeApiJsiInstall.h" + +void InstallNativeApiJSC(JSGlobalContextRef context, const NativeApiJSCConfig& config) { + if (context == nullptr) { + return; + } + Runtime runtime(context); + InstallNativeApiJSI(runtime, config); +} + +} // namespace nativescript + +extern "C" void NativeScriptInstallNativeApiJSC(JSGlobalContextRef context, + const char* metadataPath) { + nativescript::NativeApiJSCConfig config; + config.metadataPath = metadataPath; + nativescript::InstallNativeApiJSC(context, config); +} + +#endif // TARGET_ENGINE_JSC diff --git a/NativeScript/ffi/jsc/NativeApiJSCHostObjects.mm b/NativeScript/ffi/jsc/NativeApiJSCHostObjects.mm new file mode 100644 index 00000000..ab2da20a --- /dev/null +++ b/NativeScript/ffi/jsc/NativeApiJSCHostObjects.mm @@ -0,0 +1,182 @@ +#include "NativeApiJSCRuntime.h" + +#ifdef TARGET_ENGINE_JSC + +namespace facebook { +namespace jsi { + +namespace jscdirect { + +JSClassRef hostClass(Runtime& runtime); +JSClassRef functionClass(Runtime& runtime); + +JSValueRef hostGetProperty(JSContextRef context, JSObjectRef object, JSStringRef propertyName, + JSValueRef* exception) { + auto* holder = static_cast(JSObjectGetPrivate(object)); + if (holder == nullptr || holder->hostObject == nullptr) { + return nullptr; + } + Runtime runtime(holder->state); + try { + Value result = holder->hostObject->get(runtime, PropNameID(stringToUtf8(propertyName))); + return result.isUndefined() ? nullptr : result.local(runtime); + } catch (const std::exception& error) { + setException(context, exception, error); + return JSValueMakeUndefined(context); + } +} + +bool hostSetProperty(JSContextRef context, JSObjectRef object, JSStringRef propertyName, + JSValueRef value, JSValueRef* exception) { + auto* holder = static_cast(JSObjectGetPrivate(object)); + if (holder == nullptr || holder->hostObject == nullptr) { + return false; + } + Runtime runtime(holder->state); + try { + holder->hostObject->set(runtime, PropNameID(stringToUtf8(propertyName)), Value(runtime, value)); + return true; + } catch (const std::exception& error) { + setException(context, exception, error); + return true; + } +} + +void hostGetPropertyNames(JSContextRef, JSObjectRef object, + JSPropertyNameAccumulatorRef propertyNames) { + auto* holder = static_cast(JSObjectGetPrivate(object)); + if (holder == nullptr || holder->hostObject == nullptr) { + return; + } + Runtime runtime(holder->state); + try { + for (const auto& property : holder->hostObject->getPropertyNames(runtime)) { + JSStringRef name = makeJSString(property.utf8(runtime)); + JSPropertyNameAccumulatorAddName(propertyNames, name); + JSStringRelease(name); + } + } catch (const std::exception&) { + } +} + +void hostFinalize(JSObjectRef object) { + delete static_cast(JSObjectGetPrivate(object)); +} + +JSValueRef functionCall(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, + size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception) { + auto* holder = static_cast(JSObjectGetPrivate(function)); + if (holder == nullptr || !holder->callback) { + return JSValueMakeUndefined(context); + } + Runtime runtime(holder->state); + std::vector args; + args.reserve(argumentCount); + for (size_t i = 0; i < argumentCount; i++) { + args.emplace_back(runtime, arguments[i]); + } + try { + Value thisValue(runtime, thisObject); + Value result = + holder->callback(runtime, thisValue, args.empty() ? nullptr : args.data(), args.size()); + return result.local(runtime); + } catch (const std::exception& error) { + setException(context, exception, error); + return JSValueMakeUndefined(context); + } +} + +void functionFinalize(JSObjectRef object) { + delete static_cast(JSObjectGetPrivate(object)); +} + +JSClassRef hostClass(Runtime& runtime) { + auto state = runtime.state(); + if (state->hostClass == nullptr) { + JSClassDefinition definition = kJSClassDefinitionEmpty; + definition.className = "NativeScriptDirectHostObject"; + definition.getProperty = hostGetProperty; + definition.setProperty = hostSetProperty; + definition.getPropertyNames = hostGetPropertyNames; + definition.finalize = hostFinalize; + state->hostClass = JSClassCreate(&definition); + } + return state->hostClass; +} + +JSClassRef functionClass(Runtime& runtime) { + auto state = runtime.state(); + if (state->functionClass == nullptr) { + JSClassDefinition definition = kJSClassDefinitionEmpty; + definition.className = "NativeScriptDirectFunction"; + definition.callAsFunction = functionCall; + definition.finalize = functionFinalize; + state->functionClass = JSClassCreate(&definition); + } + return state->functionClass; +} + +void setFunctionPrototype(JSGlobalContextRef context, JSObjectRef function) { + if (context == nullptr || function == nullptr) { + return; + } + + JSValueRef exception = nullptr; + JSStringRef functionName = makeJSString("Function"); + JSValueRef functionValue = + JSObjectGetProperty(context, JSContextGetGlobalObject(context), functionName, &exception); + JSStringRelease(functionName); + if (exception != nullptr || functionValue == nullptr || + !JSValueIsObject(context, functionValue)) { + return; + } + + exception = nullptr; + JSObjectRef functionConstructor = JSValueToObject(context, functionValue, &exception); + if (exception != nullptr || functionConstructor == nullptr) { + return; + } + + JSStringRef prototypeName = makeJSString("prototype"); + JSValueRef prototypeValue = + JSObjectGetProperty(context, functionConstructor, prototypeName, &exception); + JSStringRelease(prototypeName); + if (exception != nullptr || prototypeValue == nullptr || + !JSValueIsObject(context, prototypeValue)) { + return; + } + + JSObjectSetPrototype(context, function, prototypeValue); +} + +} // namespace jscdirect + +Object Object::createFromHostObjectWithToken(Runtime& runtime, std::shared_ptr host, + const void* typeToken) { + auto* holder = new jscdirect::HostObjectHolder(runtime.state(), std::move(host), typeToken); + JSObjectRef object = JSObjectMake(runtime.context(), jscdirect::hostClass(runtime), holder); + return Object::fromValueStorage(Value(runtime, object).storage_); +} + +Function Function::createFromHostFunction(Runtime& runtime, const PropNameID& name, unsigned int, + HostFunctionType callback) { + auto* holder = new jscdirect::FunctionHolder(runtime.state(), std::move(callback)); + JSObjectRef function = JSObjectMake(runtime.context(), jscdirect::functionClass(runtime), holder); + jscdirect::setFunctionPrototype(runtime.context(), function); + std::string functionName = name.utf8(runtime); + if (!functionName.empty()) { + JSStringRef property = jscdirect::makeJSString("name"); + JSStringRef valueString = jscdirect::makeJSString(functionName); + JSValueRef value = JSValueMakeString(runtime.context(), valueString); + JSObjectSetProperty(runtime.context(), function, property, value, kJSPropertyAttributeReadOnly, + nullptr); + JSStringRelease(valueString); + JSStringRelease(property); + } + return Function(Object::fromValueStorage(Value(runtime, function).storage_)); +} + +} // namespace jsi +} // namespace facebook + +#endif // TARGET_ENGINE_JSC diff --git a/NativeScript/ffi/jsc/NativeApiJSCRuntime.h b/NativeScript/ffi/jsc/NativeApiJSCRuntime.h new file mode 100644 index 00000000..305399f4 --- /dev/null +++ b/NativeScript/ffi/jsc/NativeApiJSCRuntime.h @@ -0,0 +1,839 @@ +#ifndef NATIVESCRIPT_FFI_JSC_NATIVE_API_JSC_RUNTIME_H +#define NATIVESCRIPT_FFI_JSC_NATIVE_API_JSC_RUNTIME_H + +#ifdef TARGET_ENGINE_JSC + +#import +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Metadata.h" +#include "MetadataReader.h" +#include "ffi.h" + +@protocol NativeApiJsiClassBuilderProtocol +@end + +#ifdef EMBED_METADATA_SIZE +extern const unsigned char embedded_metadata[EMBED_METADATA_SIZE]; +#endif + +namespace facebook { +namespace jsi { + +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 void set(Runtime& runtime, const PropNameID& name, const Value& value); + virtual std::vector getPropertyNames(Runtime& runtime); +}; + +using HostFunctionType = std::function; + +namespace jscdirect { + +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 { + explicit RuntimeState(JSGlobalContextRef context) : context(context) {} + + ~RuntimeState() { + if (hostClass != nullptr) { + JSClassRelease(hostClass); + } + if (functionClass != nullptr) { + JSClassRelease(functionClass); + } + } + + JSGlobalContextRef context = nullptr; + JSClassRef hostClass = nullptr; + JSClassRef functionClass = nullptr; +}; + +struct ValueStorage { + enum class Kind { + Undefined, + Null, + Bool, + Number, + JSC, + }; + + explicit ValueStorage(Kind kind) : kind(kind) {} + + ~ValueStorage() { + if (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) + : 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; +}; + +} // namespace jscdirect + +class Runtime { + public: + explicit Runtime(JSGlobalContextRef context) + : state_(std::make_shared(context)) {} + + explicit Runtime(std::shared_ptr state) : state_(std::move(state)) {} + + JSGlobalContextRef context() const { return state_->context; } + std::shared_ptr state() const { return state_; } + + 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 = jscdirect::makeJSString(value); + String result(runtime, string); + JSStringRelease(string); + return result; + } + + static String createFromUtf8(Runtime& runtime, const std::string& value) { + JSStringRef string = jscdirect::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() + : storage_( + std::make_shared(jscdirect::ValueStorage::Kind::Undefined)) {} + + Value(bool value) + : storage_(std::make_shared(jscdirect::ValueStorage::Kind::Bool)) { + storage_->boolValue = value; + } + + Value(double value) + : storage_(std::make_shared(jscdirect::ValueStorage::Kind::Number)) { + storage_->numberValue = value; + } + + Value(int value) : Value(static_cast(value)) {} + Value(uint32_t value) : Value(static_cast(value)) {} + + Value(Runtime& runtime, const Value& value) : storage_(value.storage_) {} + Value(Runtime& runtime, Value&& value) : storage_(std::move(value.storage_)) {} + Value(Runtime& runtime, const String& value) : storage_(value.storage_) {} + 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) + : storage_(std::make_shared(jscdirect::ValueStorage::Kind::JSC)) { + storage_->context = runtime.context(); + storage_->value = value != nullptr ? value : JSValueMakeUndefined(runtime.context()); + JSValueProtect(runtime.context(), storage_->value); + } + + static Value undefined() { return Value(); } + static Value null() { + Value value; + value.storage_ = std::make_shared(jscdirect::ValueStorage::Kind::Null); + return value; + } + + bool isUndefined() const { + return storage_->kind == jscdirect::ValueStorage::Kind::Undefined || + (storage_->kind == jscdirect::ValueStorage::Kind::JSC && + JSValueIsUndefined(storage_->context, storage_->value)); + } + bool isNull() const { + return storage_->kind == jscdirect::ValueStorage::Kind::Null || + (storage_->kind == jscdirect::ValueStorage::Kind::JSC && + JSValueIsNull(storage_->context, storage_->value)); + } + bool isBool() const { + return storage_->kind == jscdirect::ValueStorage::Kind::Bool || + (storage_->kind == jscdirect::ValueStorage::Kind::JSC && + JSValueIsBoolean(storage_->context, storage_->value)); + } + bool getBool() const { + if (storage_->kind == jscdirect::ValueStorage::Kind::Bool) { + return storage_->boolValue; + } + if (storage_->kind == jscdirect::ValueStorage::Kind::JSC) { + return JSValueToBoolean(storage_->context, storage_->value); + } + return false; + } + bool isNumber() const { + return storage_->kind == jscdirect::ValueStorage::Kind::Number || + (storage_->kind == jscdirect::ValueStorage::Kind::JSC && + JSValueIsNumber(storage_->context, storage_->value)); + } + double getNumber() const { + if (storage_->kind == jscdirect::ValueStorage::Kind::Number) { + return storage_->numberValue; + } + if (storage_->kind == jscdirect::ValueStorage::Kind::JSC) { + return JSValueToNumber(storage_->context, storage_->value, nullptr); + } + return 0; + } + + bool isObject() const { + return storage_->kind == jscdirect::ValueStorage::Kind::JSC && + JSValueIsObject(storage_->context, storage_->value); + } + bool isString() const { + return storage_->kind == jscdirect::ValueStorage::Kind::JSC && + JSValueIsString(storage_->context, storage_->value); + } + bool isBigInt() const { + if (storage_->kind != jscdirect::ValueStorage::Kind::JSC) { + return false; + } + if (__builtin_available(macOS 15.0, iOS 18.0, *)) { + return JSValueIsBigInt(storage_->context, storage_->value); + } + return false; + } + bool isSymbol() const { + return storage_->kind == jscdirect::ValueStorage::Kind::JSC && + JSValueIsSymbol(storage_->context, storage_->value); + } + + Object asObject(Runtime& runtime) const; + String asString(Runtime& runtime) const; + BigInt getBigInt(Runtime& runtime) const; + + JSValueRef local(Runtime& runtime) const { + switch (storage_->kind) { + case jscdirect::ValueStorage::Kind::Undefined: + return JSValueMakeUndefined(runtime.context()); + case jscdirect::ValueStorage::Kind::Null: + return JSValueMakeNull(runtime.context()); + case jscdirect::ValueStorage::Kind::Bool: + return JSValueMakeBoolean(runtime.context(), storage_->boolValue); + case jscdirect::ValueStorage::Kind::Number: + return JSValueMakeNumber(runtime.context(), storage_->numberValue); + case jscdirect::ValueStorage::Kind::JSC: + return storage_->value; + } + } + + private: + friend class Runtime; + friend class Object; + friend class String; + friend class BigInt; + friend class ArrayBuffer; + friend class Function; + friend class Array; + std::shared_ptr storage_; +}; + +class Object { + public: + Object() = default; + explicit Object(Runtime& runtime) + : storage_(std::make_shared(jscdirect::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), + jscdirect::hostObjectTypeToken()); + } + + Value getProperty(Runtime& runtime, const char* name) const { + JSStringRef property = jscdirect::makeJSString(name); + JSValueRef exception = nullptr; + JSValueRef result = + JSObjectGetProperty(runtime.context(), local(runtime), property, &exception); + JSStringRelease(property); + if (exception != nullptr) { + throw JSError(runtime, jscdirect::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, jscdirect::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 = jscdirect::makeJSString(name); + JSValueRef exception = nullptr; + JSObjectSetProperty(runtime.context(), local(runtime), property, value.local(runtime), + kJSPropertyAttributeNone, &exception); + JSStringRelease(property); + if (exception != nullptr) { + throw JSError(runtime, jscdirect::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, jscdirect::valueToUtf8(runtime.context(), exception)); + } + } + + bool hasProperty(Runtime& runtime, const char* name) const { + JSStringRef property = jscdirect::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 = jscdirect::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 == jscdirect::hostObjectTypeToken(); + } + + template + std::shared_ptr getHostObject(Runtime& runtime) const { + auto holder = hostObjectHolder(runtime); + if (holder == nullptr || holder->typeToken != jscdirect::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 { + Value value; + value.storage_ = storage_; + return value; + } + + 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); + + jscdirect::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, jscdirect::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, jscdirect::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, jscdirect::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(jscdirect::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, jscdirect::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, jscdirect::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, jscdirect::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(jscdirect::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, jscdirect::valueToUtf8(runtime.context(), exception)); + } + String result(runtime, string); + JSStringRelease(string); + return result; + } + + JSValueRef local(Runtime& runtime) const { return storage_->value; } + + operator Value() const { + Value value; + value.storage_ = storage_; + return value; + } + + private: + friend class Value; + std::shared_ptr storage_; +}; + +class ArrayBuffer : public Object { + public: + ArrayBuffer(Runtime& runtime, std::shared_ptr buffer) + : Object(std::make_shared(jscdirect::ValueStorage::Kind::JSC)) { + auto* holder = new jscdirect::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, jscdirect::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 jsi +} // namespace facebook + +#endif // TARGET_ENGINE_JSC + +#endif // NATIVESCRIPT_FFI_JSC_NATIVE_API_JSC_RUNTIME_H diff --git a/NativeScript/ffi/jsc/NativeApiJSCRuntime.mm b/NativeScript/ffi/jsc/NativeApiJSCRuntime.mm new file mode 100644 index 00000000..3396af69 --- /dev/null +++ b/NativeScript/ffi/jsc/NativeApiJSCRuntime.mm @@ -0,0 +1,30 @@ +#include "NativeApiJSCRuntime.h" + +#ifdef TARGET_ENGINE_JSC + +namespace facebook { +namespace jsi { + +Object Runtime::global() { + return Object::fromValueStorage(Value(*this, JSContextGetGlobalObject(context())).storage_); +} + +Value Runtime::evaluateJavaScript(std::shared_ptr buffer, + const std::string& sourceURL) { + JSStringRef source = JSStringCreateWithUTF8CString( + buffer != nullptr ? std::string(buffer->data(), buffer->size()).c_str() : ""); + JSStringRef url = jscdirect::makeJSString(sourceURL); + JSValueRef exception = nullptr; + JSValueRef result = JSEvaluateScript(context(), source, nullptr, url, 1, &exception); + JSStringRelease(source); + JSStringRelease(url); + if (exception != nullptr) { + throw JSError(*this, jscdirect::valueToUtf8(context(), exception)); + } + return Value(*this, result); +} + +} // namespace jsi +} // namespace facebook + +#endif // TARGET_ENGINE_JSC diff --git a/NativeScript/ffi/jsc/NativeApiJSCValue.mm b/NativeScript/ffi/jsc/NativeApiJSCValue.mm new file mode 100644 index 00000000..ca407156 --- /dev/null +++ b/NativeScript/ffi/jsc/NativeApiJSCValue.mm @@ -0,0 +1,83 @@ +#include "NativeApiJSCRuntime.h" + +#ifdef TARGET_ENGINE_JSC + +namespace facebook { +namespace jsi { + +Value HostObject::get(Runtime&, const PropNameID&) { return Value::undefined(); } +void HostObject::set(Runtime&, const PropNameID&, const Value&) {} +std::vector HostObject::getPropertyNames(Runtime&) { return {}; } + +String::String(Runtime& runtime, JSStringRef string) + : storage_(std::make_shared(jscdirect::ValueStorage::Kind::JSC)) { + storage_->context = runtime.context(); + storage_->value = JSValueMakeString(runtime.context(), string); + JSValueProtect(runtime.context(), storage_->value); +} + +std::string String::utf8(Runtime& runtime) const { + return jscdirect::valueToUtf8(runtime.context(), storage_->value); +} + +String::operator Value() const { + Value value; + value.storage_ = storage_; + return value; +} + +Value::Value(Runtime&, const Object& object) : storage_(object.storage_) {} +Value::Value(Runtime&, const Function& function) : storage_(function.storage_) {} +Value::Value(Runtime&, const Array& array) : storage_(array.storage_) {} +Value::Value(Runtime&, const ArrayBuffer& arrayBuffer) : storage_(arrayBuffer.storage_) {} +Value::Value(Runtime&, const BigInt& bigint) : storage_(bigint.storage_) {} + +Object Value::asObject(Runtime&) const { return Object::fromValueStorage(storage_); } + +String Value::asString(Runtime& runtime) const { + JSValueRef exception = nullptr; + JSStringRef string = JSValueToStringCopy(runtime.context(), local(runtime), &exception); + if (string == nullptr || exception != nullptr) { + throw JSError(runtime, jscdirect::valueToUtf8(runtime.context(), exception)); + } + String result(runtime, string); + JSStringRelease(string); + return result; +} + +BigInt Value::getBigInt(Runtime& runtime) const { return BigInt(runtime, local(runtime)); } + +Function Object::getPropertyAsFunction(Runtime& runtime, const char* name) const { + return getProperty(runtime, name).asObject(runtime).asFunction(runtime); +} +Function Object::asFunction(Runtime&) const { return Function(*this); } +Array Object::getArray(Runtime&) const { return Array(*this); } +ArrayBuffer Object::getArrayBuffer(Runtime&) const { return ArrayBuffer(*this); } + +Array Object::getPropertyNames(Runtime& runtime) const { + JSPropertyNameArrayRef propertyNames = + JSObjectCopyPropertyNames(runtime.context(), local(runtime)); + size_t count = JSPropertyNameArrayGetCount(propertyNames); + Array result(runtime, count); + for (size_t i = 0; i < count; i++) { + JSStringRef name = JSPropertyNameArrayGetNameAtIndex(propertyNames, i); + result.setValueAtIndex(runtime, i, String(runtime, name)); + } + JSPropertyNameArrayRelease(propertyNames); + return result; +} + +void Object::setProperty(Runtime& runtime, const char* name, const Function& value) { + setProperty(runtime, name, Value(runtime, value)); +} +void Object::setProperty(Runtime& runtime, const char* name, const Array& value) { + setProperty(runtime, name, Value(runtime, value)); +} +void Object::setProperty(Runtime& runtime, const char* name, const ArrayBuffer& value) { + setProperty(runtime, name, Value(runtime, value)); +} + +} // namespace jsi +} // namespace facebook + +#endif // TARGET_ENGINE_JSC diff --git a/NativeScript/ffi/AutoreleasePool.h b/NativeScript/ffi/napi/AutoreleasePool.h similarity index 100% rename from NativeScript/ffi/AutoreleasePool.h rename to NativeScript/ffi/napi/AutoreleasePool.h diff --git a/NativeScript/ffi/AutoreleasePool.mm b/NativeScript/ffi/napi/AutoreleasePool.mm similarity index 100% rename from NativeScript/ffi/AutoreleasePool.mm rename to NativeScript/ffi/napi/AutoreleasePool.mm diff --git a/NativeScript/ffi/Block.h b/NativeScript/ffi/napi/Block.h similarity index 86% rename from NativeScript/ffi/Block.h rename to NativeScript/ffi/napi/Block.h index 97097831..4cd81962 100644 --- a/NativeScript/ffi/Block.h +++ b/NativeScript/ffi/napi/Block.h @@ -1,6 +1,7 @@ #ifndef BLOCK_H #define BLOCK_H +#include #include #include "Cif.h" @@ -15,6 +16,10 @@ class FunctionPointer { metagen::MDSectionOffset offset; Cif* cif; bool ownsCif = false; + bool dispatchLookupCached = false; + uint64_t dispatchLookupSignatureHash = 0; + uint64_t dispatchId = 0; + void* preparedInvoker = nullptr; static napi_value wrap(napi_env env, void* function, metagen::MDSectionOffset offset, bool isBlock); diff --git a/NativeScript/ffi/Block.mm b/NativeScript/ffi/napi/Block.mm similarity index 61% rename from NativeScript/ffi/Block.mm rename to NativeScript/ffi/napi/Block.mm index 6d60da6e..3b8697af 100644 --- a/NativeScript/ffi/Block.mm +++ b/NativeScript/ffi/napi/Block.mm @@ -1,12 +1,18 @@ #include "Block.h" #import +#include #include #include #include +#include #include #include +#include #include "Interop.h" +#include "runtime/NativeScriptException.h" #include "ObjCBridge.h" +#include "SignatureDispatch.h" +#include "TypeConv.h" #include "js_native_api.h" #include "js_native_api_types.h" #include "node_api_util.h" @@ -65,8 +71,211 @@ inline bool removeCachedBlockJsFunctionEntry(void* blockPtr, BlockJsFunctionEntr } inline void deleteBlockReferenceOnOwningLoop(const BlockJsFunctionEntry& entry) { - nativescript::DeleteReferenceOnOwningThread(entry.env, entry.bridgeState, - entry.bridgeStateToken, entry.ref); + nativescript::DeleteReferenceOnOwningThread(entry.env, entry.bridgeState, entry.bridgeStateToken, + entry.ref); +} + +inline nativescript::BlockPreparedInvoker ensureFunctionPointerPreparedInvoker( + nativescript::FunctionPointer* ref, nativescript::SignatureCallKind kind) { + if (ref == nullptr || ref->cif == nullptr || ref->cif->signatureHash == 0) { + if (ref != nullptr) { + ref->dispatchLookupCached = true; + ref->dispatchLookupSignatureHash = 0; + ref->dispatchId = 0; + ref->preparedInvoker = nullptr; + } + return nullptr; + } + + if (!ref->dispatchLookupCached || + ref->dispatchLookupSignatureHash != ref->cif->signatureHash) { + ref->dispatchLookupSignatureHash = ref->cif->signatureHash; + ref->dispatchId = + nativescript::composeSignatureDispatchId(ref->cif->signatureHash, kind, 0); + if (kind == nativescript::SignatureCallKind::BlockInvoke) { + ref->preparedInvoker = + reinterpret_cast(nativescript::lookupBlockPreparedInvoker(ref->dispatchId)); + } else { + ref->preparedInvoker = + reinterpret_cast(nativescript::lookupCFunctionPreparedInvoker(ref->dispatchId)); + } + ref->dispatchLookupCached = true; + } + + return reinterpret_cast(ref->preparedInvoker); +} + +inline const napi_value* prepareFunctionPointerInvocationArgs(napi_env env, nativescript::Cif* cif, + size_t actualArgc, + const napi_value* rawArgs, + napi_value* stackArgs, + size_t stackCapacity, + std::vector* heapArgs) { + if (cif == nullptr || cif->argc == 0) { + return nullptr; + } + + if (actualArgc == cif->argc && rawArgs != nullptr) { + return rawArgs; + } + + napi_value jsUndefined = nullptr; + napi_get_undefined(env, &jsUndefined); + + if (cif->argc <= stackCapacity) { + for (unsigned int i = 0; i < cif->argc; i++) { + stackArgs[i] = i < actualArgc && rawArgs != nullptr ? rawArgs[i] : jsUndefined; + } + return stackArgs; + } + + heapArgs->assign(cif->argc, jsUndefined); + const size_t copyArgc = std::min(actualArgc, static_cast(cif->argc)); + if (copyArgc > 0 && rawArgs != nullptr) { + memcpy(heapArgs->data(), rawArgs, copyArgc * sizeof(napi_value)); + } + return heapArgs->data(); +} + +napi_value callFunctionPointerAsCFunctionDirect(napi_env env, nativescript::FunctionPointer* ref, + size_t actualArgc, + const napi_value* rawArgs) { + if (ref == nullptr || ref->cif == nullptr || ref->function == nullptr) { + napi_throw_error(env, "NativeScriptException", "Missing native function pointer."); + return nullptr; + } + + auto cif = ref->cif; + napi_value stackInvocationArgs[16]; + std::vector heapInvocationArgs; + const napi_value* invocationArgs = prepareFunctionPointerInvocationArgs( + env, cif, actualArgc, rawArgs, stackInvocationArgs, 16, &heapInvocationArgs); + + void* stackAValues[16]; + std::vector heapAValues; + void** avalues = stackAValues; + if (cif->argc > 16) { + heapAValues.resize(cif->argc); + avalues = heapAValues.data(); + } + + bool shouldFreeAny = false; + uint8_t stackShouldFree[16] = {}; + std::vector heapShouldFree; + uint8_t* shouldFree = stackShouldFree; + if (cif->argc > 16) { + heapShouldFree.assign(cif->argc, false); + shouldFree = heapShouldFree.data(); + } + + for (unsigned int i = 0; i < cif->argc; i++) { + avalues[i] = cif->avalues[i]; + bool shouldFreeArg = false; + cif->argTypes[i]->toNative(env, invocationArgs[i], avalues[i], &shouldFreeArg, + &shouldFreeAny); + shouldFree[i] = shouldFreeArg ? 1 : 0; + } + + void* rvalue = cif->rvalue; + auto preparedInvoker = + ensureFunctionPointerPreparedInvoker(ref, nativescript::SignatureCallKind::CFunction); + + @try { + if (preparedInvoker != nullptr) { + preparedInvoker(ref->function, avalues, rvalue); + } else { + ffi_call(&cif->cif, FFI_FN(ref->function), rvalue, avalues); + } + } @catch (NSException* exception) { + std::string message = exception.description.UTF8String; + nativescript::NativeScriptException nativeScriptException(message); + nativeScriptException.ReThrowToJS(env); + return nullptr; + } + + if (shouldFreeAny) { + for (unsigned int i = 0; i < cif->argc; i++) { + if (shouldFree[i]) { + cif->argTypes[i]->free(env, *((void**)avalues[i])); + } + } + } + + return cif->returnType->toJS(env, rvalue); +} + +napi_value callFunctionPointerAsBlockDirect(napi_env env, nativescript::FunctionPointer* ref, + size_t actualArgc, + const napi_value* rawArgs) { + if (ref == nullptr || ref->cif == nullptr || ref->function == nullptr) { + napi_throw_error(env, "NativeScriptException", "Missing native block pointer."); + return nullptr; + } + + auto block = static_cast(ref->function); + if (block == nullptr || block->invoke == nullptr) { + napi_throw_error(env, "NativeScriptException", "Missing native block invoke pointer."); + return nullptr; + } + + auto cif = ref->cif; + napi_value stackInvocationArgs[16]; + std::vector heapInvocationArgs; + const napi_value* invocationArgs = prepareFunctionPointerInvocationArgs( + env, cif, actualArgc, rawArgs, stackInvocationArgs, 16, &heapInvocationArgs); + + void* stackAValues[17]; + std::vector heapAValues; + void** avalues = stackAValues; + if (cif->cif.nargs > 17) { + heapAValues.resize(cif->cif.nargs); + avalues = heapAValues.data(); + } + + bool shouldFreeAny = false; + uint8_t stackShouldFree[16] = {}; + std::vector heapShouldFree; + uint8_t* shouldFree = stackShouldFree; + if (cif->argc > 16) { + heapShouldFree.assign(cif->argc, false); + shouldFree = heapShouldFree.data(); + } + + avalues[0] = █ + for (unsigned int i = 0; i < cif->argc; i++) { + avalues[i + 1] = cif->avalues[i]; + bool shouldFreeArg = false; + cif->argTypes[i]->toNative(env, invocationArgs[i], avalues[i + 1], &shouldFreeArg, + &shouldFreeAny); + shouldFree[i] = shouldFreeArg ? 1 : 0; + } + + void* rvalue = cif->rvalue; + nativescript::BlockPreparedInvoker preparedInvoker = ensureFunctionPointerPreparedInvoker( + ref, nativescript::SignatureCallKind::BlockInvoke); + + @try { + if (preparedInvoker != nullptr) { + preparedInvoker(block->invoke, avalues, rvalue); + } else { + ffi_call(&cif->cif, FFI_FN(block->invoke), rvalue, avalues); + } + } @catch (NSException* exception) { + std::string message = exception.description.UTF8String; + nativescript::NativeScriptException nativeScriptException(message); + nativeScriptException.ReThrowToJS(env); + return nullptr; + } + + if (shouldFreeAny) { + for (unsigned int i = 0; i < cif->argc; i++) { + if (shouldFree[i]) { + cif->argTypes[i]->free(env, *((void**)avalues[i + 1])); + } + } + } + + return cif->returnType->toJS(env, rvalue); } void block_copy(void* dest, void* src) { @@ -144,8 +353,7 @@ inline void cacheBlockJsFunction(napi_env env, void* blockPtr, napi_value jsFunc entry.ref = nativescript::make_ref(env, jsFunction, 0); entry.env = env; entry.bridgeState = nativescript::ObjCBridgeState::InstanceData(env); - entry.bridgeStateToken = - entry.bridgeState != nullptr ? entry.bridgeState->lifetimeToken : 0; + entry.bridgeStateToken = entry.bridgeState != nullptr ? entry.bridgeState->lifetimeToken : 0; entry.jsThreadId = closure != nullptr ? closure->jsThreadId : std::this_thread::get_id(); entry.jsRunLoop = closure != nullptr ? closure->jsRunLoop : CFRunLoopGetCurrent(); g_blockToJsFunction[blockPtr] = entry; @@ -174,7 +382,7 @@ void block_finalize_now(napi_env env, void* data, void* hint) { free(block); } - + void finalizeFunctionPointerNow(napi_env env, void* finalize_data, void* finalize_hint) { auto ref = static_cast(finalize_data); if (ref == nullptr) { @@ -259,14 +467,23 @@ bool isObjCBlockObject(id obj) { return false; } + static thread_local std::unordered_map blockClassCache; + auto cached = blockClassCache.find(cls); + if (cached != blockClassCache.end()) { + return cached->second; + } + const char* className = class_getName(cls); if (className == nullptr) { + blockClassCache.emplace(cls, false); return false; } // Runtime block classes are typically internal names like // __NSGlobalBlock__, __NSMallocBlock__, __NSStackBlock__. - return className[0] == '_' && className[1] == '_' && strstr(className, "Block") != nullptr; + bool isBlock = className[0] == '_' && className[1] == '_' && strstr(className, "Block") != nullptr; + blockClassCache.emplace(cls, isBlock); + return isBlock; } const char* getObjCBlockSignature(void* blockPtr) { @@ -421,80 +638,35 @@ bool isObjCBlockObject(id obj) { } napi_value FunctionPointer::jsCallAsCFunction(napi_env env, napi_callback_info cbinfo) { - FunctionPointer* ref; - - napi_get_cb_info(env, cbinfo, nullptr, nullptr, nullptr, (void**)&ref); - - auto cif = ref->cif; + FunctionPointer* ref = nullptr; + size_t actualArgc = 16; + napi_value stackArgs[16]; + napi_get_cb_info(env, cbinfo, &actualArgc, stackArgs, nullptr, (void**)&ref); - size_t argc = cif->argc; - napi_get_cb_info(env, cbinfo, &argc, cif->argv, nullptr, nullptr); - - void* avalues[cif->argc]; - void* rvalue = cif->rvalue; - - bool shouldFreeAny = false; - bool shouldFree[cif->argc]; - - if (cif->argc > 0) { - for (unsigned int i = 0; i < cif->argc; i++) { - shouldFree[i] = false; - avalues[i] = cif->avalues[i]; - cif->argTypes[i]->toNative(env, cif->argv[i], avalues[i], &shouldFree[i], &shouldFreeAny); - } + if (actualArgc > 16) { + std::vector heapArgs(actualArgc); + size_t retryArgc = actualArgc; + napi_get_cb_info(env, cbinfo, &retryArgc, heapArgs.data(), nullptr, nullptr); + return callFunctionPointerAsCFunctionDirect(env, ref, retryArgc, heapArgs.data()); } - ffi_call(&cif->cif, FFI_FN(ref->function), rvalue, avalues); - - if (shouldFreeAny) { - for (unsigned int i = 0; i < cif->argc; i++) { - if (shouldFree[i]) { - cif->argTypes[i]->free(env, *((void**)avalues[i])); - } - } - } - - return cif->returnType->toJS(env, rvalue); + return callFunctionPointerAsCFunctionDirect(env, ref, actualArgc, stackArgs); } napi_value FunctionPointer::jsCallAsBlock(napi_env env, napi_callback_info cbinfo) { - FunctionPointer* ref; - - napi_get_cb_info(env, cbinfo, nullptr, nullptr, nullptr, (void**)&ref); - - Block_literal_1* block = (Block_literal_1*)ref->function; - auto cif = ref->cif; + FunctionPointer* ref = nullptr; + size_t actualArgc = 16; + napi_value stackArgs[16]; + napi_get_cb_info(env, cbinfo, &actualArgc, stackArgs, nullptr, (void**)&ref); - size_t argc = cif->argc; - napi_get_cb_info(env, cbinfo, &argc, cif->argv, nullptr, nullptr); - - void* avalues[cif->cif.nargs]; - void* rvalue = cif->rvalue; - - bool shouldFreeAny = false; - bool shouldFree[cif->argc]; - - avalues[0] = █ - - if (cif->argc > 0) { - for (unsigned int i = 0; i < cif->argc; i++) { - shouldFree[i] = false; - avalues[i + 1] = cif->avalues[i]; - cif->argTypes[i]->toNative(env, cif->argv[i], avalues[i + 1], &shouldFree[i], &shouldFreeAny); - } + if (actualArgc > 16) { + std::vector heapArgs(actualArgc); + size_t retryArgc = actualArgc; + napi_get_cb_info(env, cbinfo, &retryArgc, heapArgs.data(), nullptr, nullptr); + return callFunctionPointerAsBlockDirect(env, ref, retryArgc, heapArgs.data()); } - ffi_call(&cif->cif, FFI_FN(block->invoke), rvalue, avalues); - - if (shouldFreeAny) { - for (unsigned int i = 0; i < cif->argc; i++) { - if (shouldFree[i]) { - cif->argTypes[i]->free(env, *((void**)avalues[i + 1])); - } - } - } - - return cif->returnType->toJS(env, rvalue); + return callFunctionPointerAsBlockDirect(env, ref, actualArgc, stackArgs); } } // namespace nativescript diff --git a/NativeScript/ffi/CFunction.h b/NativeScript/ffi/napi/CFunction.h similarity index 69% rename from NativeScript/ffi/CFunction.h rename to NativeScript/ffi/napi/CFunction.h index d0003f12..269cf98a 100644 --- a/NativeScript/ffi/CFunction.h +++ b/NativeScript/ffi/napi/CFunction.h @@ -2,18 +2,25 @@ #define C_FUNCTION_H #include + #include "Cif.h" namespace nativescript { +class ObjCBridgeState; + class CFunction { public: static napi_value jsCall(napi_env env, napi_callback_info cbinfo); + static napi_value jsCallDirect(napi_env env, MDSectionOffset offset, + size_t actualArgc, + const napi_value* callArgs); CFunction(void* fnptr) : fnptr(fnptr) {} ~CFunction(); void* fnptr; + ObjCBridgeState* bridgeState = nullptr; Cif* cif = nullptr; uint8_t dispatchFlags = 0; bool dispatchLookupCached = false; diff --git a/NativeScript/ffi/CFunction.mm b/NativeScript/ffi/napi/CFunction.mm similarity index 69% rename from NativeScript/ffi/CFunction.mm rename to NativeScript/ffi/napi/CFunction.mm index 3f3a21f7..3f9d2671 100644 --- a/NativeScript/ffi/CFunction.mm +++ b/NativeScript/ffi/napi/CFunction.mm @@ -1,14 +1,19 @@ #include "CFunction.h" #include +#include +#include +#include #include +#include #include #include "Block.h" +#include "CallbackThreading.h" #include "ClassMember.h" #include "Interop.h" #include "ObjCBridge.h" #include "SignatureDispatch.h" -#include "ffi/NativeScriptException.h" -#include "ffi/Tasks.h" +#include "runtime/NativeScriptException.h" +#include "Tasks.h" #ifdef ENABLE_JS_RUNTIME #include "jsr.h" #endif @@ -17,6 +22,107 @@ namespace { +size_t getCifReturnStorageSize(Cif* cif) { + size_t size = 0; + if (cif != nullptr) { + size = cif->rvalueLength; + if (size == 0 && cif->cif.rtype != nullptr) { + size = cif->cif.rtype->size; + } + } + return size != 0 ? size : sizeof(void*); +} + +class CFunctionReturnStorage final { + public: + explicit CFunctionReturnStorage(Cif* cif) { + const size_t size = getCifReturnStorageSize(cif); + + if (size <= kInlineSize) { + rvalue_ = inlineBuffer_; + std::memset(rvalue_, 0, size); + return; + } + + rvalue_ = std::malloc(size); + if (rvalue_ != nullptr) { + std::memset(rvalue_, 0, size); + } + } + + ~CFunctionReturnStorage() { + if (rvalue_ != nullptr && rvalue_ != inlineBuffer_) { + std::free(rvalue_); + } + } + + CFunctionReturnStorage(const CFunctionReturnStorage&) = delete; + CFunctionReturnStorage& operator=(const CFunctionReturnStorage&) = delete; + + bool isValid() const { return rvalue_ != nullptr; } + void* rvalue() const { return rvalue_; } + + private: + static constexpr size_t kInlineSize = 32; + alignas(max_align_t) unsigned char inlineBuffer_[kInlineSize]; + void* rvalue_ = nullptr; +}; + +class CFunctionInvocationFrame final { + public: + explicit CFunctionInvocationFrame(Cif* cif) + : avalues_(cif != nullptr ? cif->argc : 0, nullptr), + argumentStorage_(cif != nullptr ? cif->argc : 0, nullptr) { + if (cif == nullptr) { + return; + } + + rvalue_ = std::malloc(getCifReturnStorageSize(cif)); + if (rvalue_ == nullptr) { + return; + } + + valid_ = true; + for (unsigned int i = 0; i < cif->argc; i++) { + ffi_type* argType = + cif->cif.arg_types != nullptr ? cif->cif.arg_types[i] : nullptr; + const size_t argLength = + argType != nullptr && argType->size > 0 ? argType->size : 1; + void* storage = std::malloc(argLength); + if (storage == nullptr) { + valid_ = false; + return; + } + argumentStorage_[i] = storage; + avalues_[i] = storage; + } + } + + ~CFunctionInvocationFrame() { + for (void* storage : argumentStorage_) { + if (storage != nullptr) { + std::free(storage); + } + } + if (rvalue_ != nullptr) { + std::free(rvalue_); + } + } + + CFunctionInvocationFrame(const CFunctionInvocationFrame&) = delete; + CFunctionInvocationFrame& operator=(const CFunctionInvocationFrame&) = delete; + + bool isValid() const { return valid_ && rvalue_ != nullptr; } + void* rvalue() const { return rvalue_; } + void** avalues() { return avalues_.empty() ? nullptr : avalues_.data(); } + + private: + bool valid_ = false; + void* rvalue_ = nullptr; + std::vector avalues_; + std::vector argumentStorage_; +}; + inline bool unwrapCompatNativeHandleForCFunction(napi_env env, napi_value value, void** out) { if (value == nullptr || out == nullptr) { return false; @@ -81,13 +187,10 @@ inline napi_value createCompatDispatchQueueWrapperForCFunction(napi_env env, return Pointer::create(env, reinterpret_cast(queue)); } -inline napi_value tryCallCompatLibdispatchFunction(napi_env env, napi_callback_info cbinfo, +inline napi_value tryCallCompatLibdispatchFunction(napi_env env, size_t argc, + const napi_value* argv, const char* functionName) { if (strcmp(functionName, "dispatch_get_global_queue") == 0) { - size_t argc = 2; - napi_value argv[2] = {nullptr, nullptr}; - napi_get_cb_info(env, cbinfo, &argc, argv, nullptr, nullptr); - int64_t identifier = 0; if (argc > 0) { napi_valuetype identifierType = napi_undefined; @@ -142,10 +245,6 @@ inline napi_value tryCallCompatLibdispatchFunction(napi_env env, napi_callback_i } if (strcmp(functionName, "dispatch_async") == 0) { - size_t argc = 2; - napi_value argv[2] = {nullptr, nullptr}; - napi_get_cb_info(env, cbinfo, &argc, argv, nullptr, nullptr); - if (argc < 2) { napi_throw_type_error(env, nullptr, "dispatch_async expects a queue and callback."); return nullptr; @@ -240,7 +339,9 @@ inline void ensureCFunctionDispatchLookup(CFunction* function, Cif* cif) { metadata->signaturesOffset + metadata->getOffset(offset + sizeof(MDSectionOffset)); MDFunctionFlag functionFlags = metadata->getFunctionFlag(offset + sizeof(MDSectionOffset) * 2); - auto cFunction = new CFunction(dlsym(self_dl, metadata->getString(offset))); + const char* name = metadata->getString(offset); + auto cFunction = new CFunction(dlsym(self_dl, name)); + cFunction->bridgeState = this; cFunction->cif = getCFunctionCif(env, sigOffset); cFunction->dispatchFlags = (functionFlags & mdFunctionReturnOwned) != 0 ? 1 : 0; cFunctionCache[offset] = cFunction; @@ -249,18 +350,39 @@ inline void ensureCFunctionDispatchLookup(CFunction* function, Cif* cif) { } napi_value CFunction::jsCall(napi_env env, napi_callback_info cbinfo) { - void* _offset; + void* _offset = nullptr; + size_t actualArgc = 16; + napi_value stackArgs[16]; - napi_get_cb_info(env, cbinfo, nullptr, nullptr, nullptr, &_offset); + napi_get_cb_info(env, cbinfo, &actualArgc, stackArgs, nullptr, &_offset); - auto bridgeState = ObjCBridgeState::InstanceData(env); MDSectionOffset offset = (MDSectionOffset)((size_t)_offset); + if (actualArgc > 16) { + std::vector dynamicArgs(actualArgc); + size_t retryArgc = actualArgc; + napi_get_cb_info(env, cbinfo, &retryArgc, dynamicArgs.data(), nullptr, nullptr); + dynamicArgs.resize(retryArgc); + return jsCallDirect(env, offset, retryArgc, dynamicArgs.data()); + } + + return jsCallDirect(env, offset, actualArgc, stackArgs); +} + +napi_value CFunction::jsCallDirect(napi_env env, MDSectionOffset offset, + size_t actualArgc, + const napi_value* callArgs) { + auto bridgeState = ObjCBridgeState::InstanceData(env); + if (bridgeState == nullptr) { + napi_throw_error(env, "NativeScriptException", "Missing Objective-C bridge state."); + return nullptr; + } + auto name = bridgeState->metadata->getString(offset); if (strcmp(name, "dispatch_async") == 0 || strcmp(name, "dispatch_get_current_queue") == 0 || strcmp(name, "dispatch_get_global_queue") == 0) { - return tryCallCompatLibdispatchFunction(env, cbinfo, name); + return tryCallCompatLibdispatchFunction(env, actualArgc, callArgs, name); } auto func = bridgeState->getCFunction(env, offset); @@ -274,23 +396,8 @@ inline void ensureCFunctionDispatchLookup(CFunction* function, Cif* cif) { bridgeState->metadata->getFunctionFlag(offset + sizeof(MDSectionOffset) * 2); const napi_value* invocationArgs = nullptr; - std::vector dynamicArgs; std::vector paddedArgs; - napi_value stackArgs[16]; if (cif->argc > 0) { - size_t actualArgc = 16; - napi_get_cb_info(env, cbinfo, &actualArgc, stackArgs, nullptr, nullptr); - - const napi_value* callArgs = stackArgs; - if (actualArgc > 16) { - dynamicArgs.resize(actualArgc); - size_t retryArgc = actualArgc; - napi_get_cb_info(env, cbinfo, &retryArgc, dynamicArgs.data(), nullptr, nullptr); - dynamicArgs.resize(retryArgc); - actualArgc = retryArgc; - callArgs = dynamicArgs.data(); - } - invocationArgs = callArgs; if (actualArgc != cif->argc) { napi_value jsUndefined = nullptr; @@ -312,47 +419,58 @@ inline void ensureCFunctionDispatchLookup(CFunction* function, Cif* cif) { const bool isMainEntrypoint = strcmp(name, "UIApplicationMain") == 0 || strcmp(name, "NSApplicationMain") == 0; - if (napiInvoker != nullptr && !cif->skipGeneratedNapiDispatch && !isMainEntrypoint) { + if (napiInvoker != nullptr && !cif->skipGeneratedNapiDispatch && + !isMainEntrypoint) { + CFunctionReturnStorage returnStorage(cif); + if (!returnStorage.isValid()) { + napi_throw_error(env, "NativeScriptException", + "Unable to allocate C function return storage."); + return nullptr; + } + + void* rvalue = returnStorage.rvalue(); @try { - if (!napiInvoker(env, cif, func->fnptr, invocationArgs, cif->rvalue)) { + NativeCallRuntimeUnlockScope unlockRuntime(env); + bool invoked = napiInvoker(env, cif, func->fnptr, invocationArgs, rvalue); + if (!invoked) { return nullptr; } } @catch (NSException* exception) { std::string message = exception.description.UTF8String; - NSLog(@"ObjC->JS: Exception in CFunction (direct): %s", message.c_str()); + NSLog(@"ObjC->JS: Exception in CFunction (napi): %s", message.c_str()); nativescript::NativeScriptException nativeScriptException(message); nativeScriptException.ReThrowToJS(env); return nullptr; } - - return cif->returnType->toJS(env, cif->rvalue, toJSFlags); + return cif->returnType->toJS(env, rvalue, toJSFlags); } - void* avalues[cif->argc]; - void* rvalue = cif->rvalue; + auto invocationFrame = std::make_shared(cif); + if (!invocationFrame->isValid()) { + napi_throw_error(env, "NativeScriptException", + "Unable to allocate C function invocation storage."); + return nullptr; + } + void* rvalue = invocationFrame->rvalue(); + void** avalues = invocationFrame->avalues(); bool shouldFreeAny = false; - bool shouldFree[cif->argc]; + std::vector shouldFree(cif->argc, 0); if (cif->argc > 0) { for (unsigned int i = 0; i < cif->argc; i++) { - shouldFree[i] = false; - avalues[i] = cif->avalues[i]; - cif->argTypes[i]->toNative(env, invocationArgs[i], avalues[i], &shouldFree[i], + bool argShouldFree = false; + cif->argTypes[i]->toNative(env, invocationArgs[i], avalues[i], &argShouldFree, &shouldFreeAny); + shouldFree[i] = argShouldFree ? 1 : 0; } } #ifdef ENABLE_JS_RUNTIME if (isMainEntrypoint) { - void** avaluesPtr = new void*[cif->argc]; - memcpy(avaluesPtr, avalues, cif->argc * sizeof(void*)); - - Tasks::Register([env, cif, func, preparedInvoker, rvalue, avaluesPtr]() { - void* avalues[cif->argc]; - memcpy(avalues, avaluesPtr, cif->argc * sizeof(void*)); - delete[] avaluesPtr; - + Tasks::Register([env, cif, func, preparedInvoker, invocationFrame]() { + void** avalues = invocationFrame->avalues(); + void* rvalue = invocationFrame->rvalue(); @try { if (preparedInvoker != nullptr) { preparedInvoker(func->fnptr, avalues, rvalue); @@ -373,6 +491,7 @@ inline void ensureCFunctionDispatchLookup(CFunction* function, Cif* cif) { #endif @try { + NativeCallRuntimeUnlockScope unlockRuntime(env); if (preparedInvoker != nullptr) { preparedInvoker(func->fnptr, avalues, rvalue); } else { diff --git a/NativeScript/ffi/napi/CallbackThreading.h b/NativeScript/ffi/napi/CallbackThreading.h new file mode 100644 index 00000000..63c17990 --- /dev/null +++ b/NativeScript/ffi/napi/CallbackThreading.h @@ -0,0 +1,168 @@ +#ifndef CALLBACK_THREADING_H +#define CALLBACK_THREADING_H + +#include "js_native_api.h" + +#include +#include + +#if defined(ENABLE_JS_RUNTIME) +#include "jsr.h" +#endif + +namespace nativescript { + +namespace detail { + +#if defined(ENABLE_JS_RUNTIME) && defined(TARGET_ENGINE_HERMES) +inline std::atomic native_call_unlocked_runtime_count{0}; +inline thread_local int native_caller_thread_callback_depth = 0; +#endif + +} // namespace detail + +inline bool isNativeCallRuntimeUnlockedForCallbacks() { +#if defined(ENABLE_JS_RUNTIME) && defined(TARGET_ENGINE_HERMES) + return detail::native_call_unlocked_runtime_count.load( + std::memory_order_acquire) > 0; +#else + return false; +#endif +} + +inline bool isNativeCallerThreadCallbackActive() { +#if defined(ENABLE_JS_RUNTIME) && defined(TARGET_ENGINE_HERMES) + return detail::native_caller_thread_callback_depth > 0; +#else + return false; +#endif +} + +inline bool shouldInvokeCallbackOnNativeCallerThread() { +#if defined(ENABLE_JS_RUNTIME) && defined(TARGET_ENGINE_HERMES) + return isNativeCallRuntimeUnlockedForCallbacks() || + isNativeCallerThreadCallbackActive(); +#else + return false; +#endif +} + +class NativeCallRuntimeUnlockScope final { + public: + explicit NativeCallRuntimeUnlockScope(napi_env env) { +#if defined(ENABLE_JS_RUNTIME) && defined(TARGET_ENGINE_HERMES) + if (isNativeCallerThreadCallbackActive()) { + return; + } + + auto it = JSR::env_to_jsr_cache.find(env); + if (it == JSR::env_to_jsr_cache.end() || it->second == nullptr) { + return; + } + + jsr_ = it->second; + unlockedDepth_ = js_current_env_lock_depth(env); + for (int i = 0; i < unlockedDepth_; i++) { + jsr_->unlock(); + } + if (unlockedDepth_ == 0 && jsr_->runtime != nullptr) { + runtime_ = jsr_->runtime.get(); + runtime_->unlock(); + unlockedRuntime_ = true; + } + if (unlockedDepth_ > 0 || unlockedRuntime_) { + didUnlock_ = true; + detail::native_call_unlocked_runtime_count.fetch_add( + 1, std::memory_order_release); + } +#else + (void)env; +#endif + } + + ~NativeCallRuntimeUnlockScope() { +#if defined(ENABLE_JS_RUNTIME) && defined(TARGET_ENGINE_HERMES) + if (didUnlock_) { + detail::native_call_unlocked_runtime_count.fetch_sub( + 1, std::memory_order_release); + } + if (jsr_ != nullptr) { + for (int i = 0; i < unlockedDepth_; i++) { + jsr_->lock(); + } + } + if (unlockedRuntime_ && runtime_ != nullptr) { + runtime_->lock(); + } +#endif + } + + NativeCallRuntimeUnlockScope(const NativeCallRuntimeUnlockScope&) = delete; + NativeCallRuntimeUnlockScope& operator=(const NativeCallRuntimeUnlockScope&) = + delete; + + private: +#if defined(ENABLE_JS_RUNTIME) && defined(TARGET_ENGINE_HERMES) + JSR* jsr_ = nullptr; + facebook::jsi::ThreadSafeRuntime* runtime_ = nullptr; +#endif + int unlockedDepth_ = 0; + bool unlockedRuntime_ = false; + bool didUnlock_ = false; +}; + +class NativeCallbackScope final { + public: + explicit NativeCallbackScope(napi_env env) : env_(env) { +#if defined(ENABLE_JS_RUNTIME) && defined(TARGET_ENGINE_HERMES) + if (isNativeCallerThreadCallbackActive() || + js_current_env_lock_depth(env_) > 0) { + napi_open_handle_scope(env_, &napiHandleScope_); + return; + } + + auto it = JSR::env_to_jsr_cache.find(env_); + if (it != JSR::env_to_jsr_cache.end() && it->second != nullptr) { + jsr_ = it->second; + jsr_->lock(); + detail::native_caller_thread_callback_depth += 1; + napi_open_handle_scope(env_, &napiHandleScope_); + return; + } +#endif +#if defined(ENABLE_JS_RUNTIME) + napiScope_ = std::make_unique(env_); +#else + (void)env_; +#endif + } + + ~NativeCallbackScope() { +#if defined(ENABLE_JS_RUNTIME) && defined(TARGET_ENGINE_HERMES) + if (napiHandleScope_ != nullptr) { + napi_close_handle_scope(env_, napiHandleScope_); + } + if (jsr_ != nullptr) { + detail::native_caller_thread_callback_depth -= 1; + jsr_->unlock(); + } +#endif + } + + NativeCallbackScope(const NativeCallbackScope&) = delete; + NativeCallbackScope& operator=(const NativeCallbackScope&) = delete; + + private: + napi_env env_; +#if defined(ENABLE_JS_RUNTIME) && defined(TARGET_ENGINE_HERMES) + JSR* jsr_ = nullptr; + napi_handle_scope napiHandleScope_ = nullptr; +#endif +#if defined(ENABLE_JS_RUNTIME) + std::unique_ptr napiScope_; +#endif +}; + +} // namespace nativescript + +#endif // CALLBACK_THREADING_H diff --git a/NativeScript/ffi/Cif.h b/NativeScript/ffi/napi/Cif.h similarity index 89% rename from NativeScript/ffi/Cif.h rename to NativeScript/ffi/napi/Cif.h index ee483acf..6e4d5679 100644 --- a/NativeScript/ffi/Cif.h +++ b/NativeScript/ffi/napi/Cif.h @@ -21,6 +21,8 @@ class Cif { bool isVariadic = false; uint64_t signatureHash = 0; bool skipGeneratedNapiDispatch = false; + bool generatedDispatchHasRoundTripCacheArgument = false; + bool generatedDispatchUsesObjectReturnStorage = false; void* rvalue; void** avalues; diff --git a/NativeScript/ffi/Cif.mm b/NativeScript/ffi/napi/Cif.mm similarity index 94% rename from NativeScript/ffi/Cif.mm rename to NativeScript/ffi/napi/Cif.mm index 6f98b49f..dfe18c8c 100644 --- a/NativeScript/ffi/Cif.mm +++ b/NativeScript/ffi/napi/Cif.mm @@ -64,6 +64,10 @@ inline bool typeRequiresSlowGeneratedNapiDispatch(const std::shared_ptrkind) { case mdTypeUChar: case mdTypeUInt8: + case mdTypeString: + case mdTypePointer: + case mdTypeStruct: + case mdTypeArray: case mdTypeBlock: case mdTypeFunctionPointer: case mdTypeVector: @@ -75,17 +79,43 @@ inline bool typeRequiresSlowGeneratedNapiDispatch(const std::shared_ptrskipGeneratedNapiDispatch = false; + cif->generatedDispatchHasRoundTripCacheArgument = false; + cif->generatedDispatchUsesObjectReturnStorage = false; + + if (cif->returnType != nullptr) { + cif->generatedDispatchUsesObjectReturnStorage = + typeKindMayUseRoundTripCache(cif->returnType->kind); + } + cif->skipGeneratedNapiDispatch = typeRequiresSlowGeneratedNapiDispatch(cif->returnType); if (cif->skipGeneratedNapiDispatch) { return; } for (const auto& argType : cif->argTypes) { + if (argType != nullptr && typeKindMayUseRoundTripCache(argType->kind)) { + cif->generatedDispatchHasRoundTripCacheArgument = true; + } if (typeRequiresSlowGeneratedNapiDispatch(argType)) { cif->skipGeneratedNapiDispatch = true; return; diff --git a/NativeScript/ffi/Class.h b/NativeScript/ffi/napi/Class.h similarity index 100% rename from NativeScript/ffi/Class.h rename to NativeScript/ffi/napi/Class.h diff --git a/NativeScript/ffi/Class.mm b/NativeScript/ffi/napi/Class.mm similarity index 99% rename from NativeScript/ffi/Class.mm rename to NativeScript/ffi/napi/Class.mm index cd435952..12aea823 100644 --- a/NativeScript/ffi/Class.mm +++ b/NativeScript/ffi/napi/Class.mm @@ -146,7 +146,7 @@ inline bool tryGetInteropPointerArg(napi_env env, napi_value value, void** out) ClassBuilder* builder = new ClassBuilder(env, argv[0]); // It gets lazily built when a static method is called. // builder->build(); - bridgeState->classesByPointer[builder->nativeClass] = builder; + bridgeState->registerRuntimeClass(builder, builder->nativeClass); return nullptr; } @@ -599,14 +599,14 @@ static napi_value fillStack(napi_env env, napi_callback_info cbinfo) { objects:self->stackbuf count:16]; - for (NSUInteger index = 0; index < count; index++) { + for (uint32_t index = 0; index < static_cast(count); index++) { id obj = self->state.itemsPtr[index]; napi_value jsObj = bridgeState->getObject(env, obj); napi_set_element(env, stackArray, index, jsObj); } napi_value result; - napi_create_int32(env, count, &result); + napi_create_uint32(env, static_cast(count), &result); return result; } @@ -747,8 +747,7 @@ void defineProtocolMembers(napi_env env, ObjCClassMemberMap& members, napi_value if (nativeClass != nil) { napi_wrap(env, constructor, (void*)nativeClass, nil, nil, nil); - bridgeState->classesByPointer[nativeClass] = this; - bridgeState->mdClassesByPointer[nativeClass] = metadataOffset; + bridgeState->registerRuntimeClass(this, nativeClass); } napi_get_named_property(env, constructor, "prototype", &prototype); diff --git a/NativeScript/ffi/ClassBuilder.h b/NativeScript/ffi/napi/ClassBuilder.h similarity index 100% rename from NativeScript/ffi/ClassBuilder.h rename to NativeScript/ffi/napi/ClassBuilder.h diff --git a/NativeScript/ffi/ClassBuilder.mm b/NativeScript/ffi/napi/ClassBuilder.mm similarity index 92% rename from NativeScript/ffi/ClassBuilder.mm rename to NativeScript/ffi/napi/ClassBuilder.mm index f11cfe5e..de9a87b8 100644 --- a/NativeScript/ffi/ClassBuilder.mm +++ b/NativeScript/ffi/napi/ClassBuilder.mm @@ -128,6 +128,58 @@ napi_env resolveClassBuilderEnv(id self) { }) )"; +const char* kInstallClassFromStringAliasSource = R"( + (function (global) { + if (global.__nsClassFromStringAliasInstalled === true) { + return; + } + + const original = global.NSClassFromString; + if (typeof original !== "function") { + return; + } + + Object.defineProperty(global, "__nsOriginalNSClassFromString", { + configurable: true, + enumerable: false, + writable: false, + value: original + }); + + Object.defineProperty(global, "NSClassFromString", { + configurable: true, + enumerable: true, + writable: true, + value: function (name) { + const cls = original.call(this, name); + if (cls != null) { + try { + const registry = global.__nsConstructorsByObjCClassName; + const stringFromClass = global.NSStringFromClass; + if (registry != null && typeof stringFromClass === "function") { + const runtimeName = stringFromClass(cls); + const constructor = registry[runtimeName]; + if (constructor != null) { + return constructor; + } + } + } catch (_) { + } + } + + return cls; + } + }); + + Object.defineProperty(global, "__nsClassFromStringAliasInstalled", { + configurable: true, + enumerable: false, + writable: false, + value: true + }); + }) +)"; + const char* NSFastEnumerationMethodEncoding() { static const char* encoding = nullptr; if (encoding == nullptr) { @@ -856,24 +908,6 @@ NSUInteger JS_SymbolIteratorCountByEnumerating(id self, SEL _cmd, NSFastEnumerat } } - bool hasOwnOverrides = false; - napi_value overridePropertyNames = nullptr; - napi_get_all_property_names(env, args[0], napi_key_own_only, napi_key_skip_symbols, - napi_key_numbers_to_strings, &overridePropertyNames); - uint32_t overridePropertyCount = 0; - napi_get_array_length(env, overridePropertyNames, &overridePropertyCount); - hasOwnOverrides = overridePropertyCount > 0; - - bool hasExposedMethodsOption = false; - bool hasProtocolsOption = false; - if (hasOptionsObject) { - napi_has_named_property(env, options, "exposedMethods", &hasExposedMethodsOption); - napi_has_named_property(env, options, "protocols", &hasProtocolsOption); - } - - bool shouldReuseExistingClass = false; - Class existingExternalClass = nullptr; - // Create a class name. napi_value baseClassName; napi_get_named_property(env, thisArg, "name", &baseClassName); @@ -902,22 +936,23 @@ NSUInteger JS_SymbolIteratorCountByEnumerating(id self, SEL _cmd, NSFastEnumerat } std::string newClassName; + bool shouldAliasRequestedName = false; if (!requestedName.empty()) { newClassName = requestedName; Class existingClass = objc_lookUpClass(newClassName.c_str()); - if (existingClass != nullptr && - class_conformsToProtocol(existingClass, @protocol(ObjCBridgeClassBuilderProtocol))) { + auto nextAvailableClassName = [](const std::string& baseName) { size_t suffix = 1; std::string candidate; do { - candidate = requestedName + std::to_string(suffix++); + candidate = baseName + std::to_string(suffix++); } while (objc_lookUpClass(candidate.c_str()) != nullptr); - newClassName = candidate; - } else if (existingClass != nullptr && !hasOwnOverrides && !hasExposedMethodsOption && - !hasProtocolsOption) { - // Name-only extensions should resolve to an existing external class when present. - shouldReuseExistingClass = true; - existingExternalClass = existingClass; + return candidate; + }; + if (existingClass != nullptr) { + newClassName = nextAvailableClassName(requestedName); + shouldAliasRequestedName = + !class_conformsToProtocol(existingClass, + @protocol(ObjCBridgeClassBuilderProtocol)); } } else { newClassName = baseClassNameBuf; @@ -1000,14 +1035,27 @@ NSUInteger JS_SymbolIteratorCountByEnumerating(id self, SEL _cmd, NSFastEnumerat napi_get_named_property(env, registryGlobal, "__nsConstructorsByObjCClassName", &classRegistry); } - if (classRegistry != nullptr) { - napi_set_named_property(env, classRegistry, newClassName.c_str(), newConstructor); + napi_value installClassFromStringAliasScript = nullptr; + napi_value installClassFromStringAlias = nullptr; + if (napi_create_string_utf8(env, kInstallClassFromStringAliasSource, NAPI_AUTO_LENGTH, + &installClassFromStringAliasScript) == napi_ok && + napi_run_script(env, installClassFromStringAliasScript, &installClassFromStringAlias) == + napi_ok && + installClassFromStringAlias != nullptr) { + napi_value aliasArgs[] = {registryGlobal}; + if (napi_call_function(env, registryGlobal, installClassFromStringAlias, 1, aliasArgs, + nullptr) != napi_ok) { + clearPendingException(env); + } + } else { + clearPendingException(env); } - if (shouldReuseExistingClass && existingExternalClass != nullptr) { - napi_remove_wrap(env, newConstructor, nullptr); - napi_wrap(env, newConstructor, (void*)existingExternalClass, nullptr, nullptr, nullptr); - return newConstructor; + if (classRegistry != nullptr) { + napi_set_named_property(env, classRegistry, newClassName.c_str(), newConstructor); + if (shouldAliasRequestedName && !requestedName.empty()) { + napi_set_named_property(env, classRegistry, requestedName.c_str(), newConstructor); + } } // Use ClassBuilder to create the native class and bridge the methods @@ -1016,7 +1064,7 @@ NSUInteger JS_SymbolIteratorCountByEnumerating(id self, SEL _cmd, NSFastEnumerat // Register the builder in the bridge state bridgeState = ObjCBridgeState::InstanceData(env); - bridgeState->classesByPointer[builder->nativeClass] = builder; + bridgeState->registerRuntimeClass(builder, builder->nativeClass); return newConstructor; } diff --git a/NativeScript/ffi/ClassMember.h b/NativeScript/ffi/napi/ClassMember.h similarity index 83% rename from NativeScript/ffi/ClassMember.h rename to NativeScript/ffi/napi/ClassMember.h index aea44b72..814e3496 100644 --- a/NativeScript/ffi/ClassMember.h +++ b/NativeScript/ffi/napi/ClassMember.h @@ -33,6 +33,8 @@ class MethodDescriptor { uint64_t dispatchId = 0; void* preparedInvoker = nullptr; void* napiInvoker = nullptr; + bool nserrorOutSignatureCached = false; + bool nserrorOutSignature = false; MethodDescriptor() {} @@ -62,7 +64,8 @@ struct ObjCClassMemberOverload { MethodDescriptor method; Cif* cif = nullptr; - ObjCClassMemberOverload(SEL selector, MDSectionOffset offset, uint8_t dispatchFlags) + ObjCClassMemberOverload(SEL selector, MDSectionOffset offset, + uint8_t dispatchFlags) : method(selector, offset) { method.dispatchFlags = dispatchFlags; } @@ -79,6 +82,14 @@ class ObjCClassMember { static napi_value jsGetter(napi_env env, napi_callback_info cbinfo); static napi_value jsReadOnlySetter(napi_env env, napi_callback_info cbinfo); static napi_value jsSetter(napi_env env, napi_callback_info cbinfo); + static napi_value jsCallDirect(napi_env env, ObjCClassMember* method, + napi_value jsThis, size_t actualArgc, + const napi_value* callArgs); + static napi_value jsGetterDirect(napi_env env, ObjCClassMember* method, + napi_value jsThis); + static napi_value jsSetterDirect(napi_env env, ObjCClassMember* method, + napi_value jsThis, napi_value value); + static napi_value jsReadOnlySetterDirect(napi_env env); void addOverload(SEL selector, MDSectionOffset offset, uint8_t dispatchFlags); ObjCClassMember(ObjCBridgeState* bridgeState, SEL selector, diff --git a/NativeScript/ffi/ClassMember.mm b/NativeScript/ffi/napi/ClassMember.mm similarity index 92% rename from NativeScript/ffi/ClassMember.mm rename to NativeScript/ffi/napi/ClassMember.mm index 59f9f6a2..e7bc296a 100644 --- a/NativeScript/ffi/ClassMember.mm +++ b/NativeScript/ffi/napi/ClassMember.mm @@ -9,8 +9,10 @@ #include #include #include +#include #include #include "ClassBuilder.h" +#include "CallbackThreading.h" #include "Closure.h" #include "Interop.h" #include "MetadataReader.h" @@ -18,9 +20,9 @@ #include "SignatureDispatch.h" #include "TypeConv.h" #include "Util.h" -#include "ffi/Block.h" -#include "ffi/Class.h" -#include "ffi/NativeScriptException.h" +#include "Block.h" +#include "Class.h" +#include "runtime/NativeScriptException.h" #include "js_native_api.h" #include "js_native_api_types.h" #include "node_api_util.h" @@ -47,7 +49,8 @@ napi_value JS_NSObject_alloc(napi_env env, napi_callback_info cbinfo) { method->cls->nativeClass != nil) { bool canFallbackToMethodClass = true; napi_valuetype jsType = napi_undefined; - if (jsThis != nullptr && napi_typeof(env, jsThis, &jsType) == napi_ok && jsType == napi_function) { + if (jsThis != nullptr && napi_typeof(env, jsThis, &jsType) == napi_ok && + jsType == napi_function) { napi_value definingConstructor = get_ref_value(env, method->cls->constructor); if (definingConstructor != nullptr) { bool isSameConstructor = false; @@ -318,7 +321,7 @@ inline bool tryObjCNapiDispatch(napi_env env, Cif* cif, id self, bool classMetho *didInvoke = false; } - if (cif == nullptr || cif->signatureHash == 0 || cif->skipGeneratedNapiDispatch) { + if (cif == nullptr || cif->signatureHash == 0) { return true; } @@ -346,16 +349,21 @@ inline bool tryObjCNapiDispatch(napi_env env, Cif* cif, id self, bool classMetho } } - auto invoker = descriptor != nullptr - ? reinterpret_cast(descriptor->napiInvoker) - : lookupObjCNapiInvoker(composeSignatureDispatchId( - cif->signatureHash, SignatureCallKind::ObjCMethod, dispatchFlags)); + ObjCNapiInvoker invoker = + !cif->skipGeneratedNapiDispatch + ? (descriptor != nullptr + ? reinterpret_cast(descriptor->napiInvoker) + : lookupObjCNapiInvoker(composeSignatureDispatchId( + cif->signatureHash, SignatureCallKind::ObjCMethod, dispatchFlags))) + : nullptr; if (invoker == nullptr) { return true; } @try { - if (!invoker(env, cif, (void*)objc_msgSend, self, selector, argv, rvalue)) { + NativeCallRuntimeUnlockScope unlockRuntime(env); + bool invoked = invoker(env, cif, (void*)objc_msgSend, self, selector, argv, rvalue); + if (!invoked) { return false; } } @catch (NSException* exception) { @@ -422,6 +430,7 @@ inline bool objcNativeCall(napi_env env, Cif* cif, id self, bool classMethod, : lookupObjCPreparedInvoker(composeSignatureDispatchId( cif->signatureHash, SignatureCallKind::ObjCMethod, dispatchFlags)); if (invoker != nullptr) { + NativeCallRuntimeUnlockScope unlockRuntime(env); invoker((void*)objc_msgSend, avalues, rvalue); return true; } @@ -429,11 +438,14 @@ inline bool objcNativeCall(napi_env env, Cif* cif, id self, bool classMethod, #if defined(__x86_64__) if (isStret) { + NativeCallRuntimeUnlockScope unlockRuntime(env); ffi_call(&cif->cif, FFI_FN(objc_msgSend_stret), rvalue, avalues); } else { + NativeCallRuntimeUnlockScope unlockRuntime(env); ffi_call(&cif->cif, FFI_FN(objc_msgSend), rvalue, avalues); } #else + NativeCallRuntimeUnlockScope unlockRuntime(env); ffi_call(&cif->cif, FFI_FN(objc_msgSend), rvalue, avalues); #endif } else { @@ -444,11 +456,14 @@ inline bool objcNativeCall(napi_env env, Cif* cif, id self, bool classMethod, avalues[0] = (void*)&superobjPtr; #if defined(__x86_64__) if (isStret) { + NativeCallRuntimeUnlockScope unlockRuntime(env); ffi_call(&cif->cif, FFI_FN(objc_msgSendSuper_stret), rvalue, avalues); } else { + NativeCallRuntimeUnlockScope unlockRuntime(env); ffi_call(&cif->cif, FFI_FN(objc_msgSendSuper), rvalue, avalues); } #else + NativeCallRuntimeUnlockScope unlockRuntime(env); ffi_call(&cif->cif, FFI_FN(objc_msgSendSuper), rvalue, avalues); #endif } @@ -598,7 +613,7 @@ inline bool isNSErrorOutMethodSignature(SEL selector, Cif* cif) { } auto lastArgType = cif->argTypes[cif->argc - 1]; - return lastArgType != nullptr && lastArgType->kind == mdTypePointer; + return lastArgType != nullptr && lastArgType->type == &ffi_type_pointer; } inline void throwArgumentsCountError(napi_env env, size_t actualCount, size_t expectedCount) { @@ -961,8 +976,7 @@ inline id assertSelf(napi_env env, napi_value jsThis, ObjCClassMember* method = napi_value definingConstructor = get_ref_value(env, method->cls->constructor); if (definingConstructor != nullptr) { bool isSameConstructor = false; - if (napi_strict_equals(env, jsThis, definingConstructor, &isSameConstructor) == - napi_ok && + if (napi_strict_equals(env, jsThis, definingConstructor, &isSameConstructor) == napi_ok && !isSameConstructor) { shouldUseClassFallback = false; } @@ -1068,6 +1082,10 @@ inline id assertSelf(napi_env env, napi_value jsThis, ObjCClassMember* method = ObjCBridgeState* bridgeState_; }; +inline bool generatedDispatchNeedsRoundTripCacheFrame(Cif* cif) { + return cif != nullptr && cif->generatedDispatchHasRoundTripCacheArgument; +} + namespace { inline size_t alignUpSize(size_t value, size_t alignment) { @@ -1413,20 +1431,37 @@ explicit CifReturnStorage(Cif* cif) { napi_value ObjCClassMember::jsCall(napi_env env, napi_callback_info cbinfo) { napi_value jsThis; - ObjCClassMember* method; + ObjCClassMember* method = nullptr; size_t actualArgc = 16; napi_value stackArgs[16]; napi_get_cb_info(env, cbinfo, &actualArgc, stackArgs, &jsThis, (void**)&method); + if (actualArgc > 16) { + std::vector dynamicArgs(actualArgc); + size_t argcRetry = actualArgc; + napi_get_cb_info(env, cbinfo, &argcRetry, dynamicArgs.data(), &jsThis, (void**)&method); + dynamicArgs.resize(argcRetry); + return jsCallDirect(env, method, jsThis, argcRetry, dynamicArgs.data()); + } + + return jsCallDirect(env, method, jsThis, actualArgc, stackArgs); +} + +napi_value ObjCClassMember::jsCallDirect(napi_env env, ObjCClassMember* method, + napi_value jsThis, size_t actualArgc, + const napi_value* rawCallArgs) { + if (method == nullptr) { + napi_throw_error(env, "NativeScriptException", "Missing Objective-C method metadata."); + return nullptr; + } + id self = assertSelf(env, jsThis, method); if (self == nullptr) { return nullptr; } - RoundTripCacheFrameGuard roundTripCacheFrame(env, method->bridgeState); - const bool receiverIsClass = object_isClass(self); Class receiverClass = receiverIsClass ? (Class)self : [self class]; auto resolveDescriptorCif = [&](MethodDescriptor* descriptor, Cif** cacheSlot) -> Cif* { @@ -1454,17 +1489,10 @@ explicit CifReturnStorage(Cif* cif) { return resolved; }; - const napi_value* callArgs = stackArgs; + const napi_value* callArgs = actualArgc > 0 ? rawCallArgs : nullptr; std::vector dynamicArgs; - if (actualArgc > 16) { - dynamicArgs.resize(actualArgc); - size_t argcRetry = actualArgc; - napi_get_cb_info(env, cbinfo, &argcRetry, dynamicArgs.data(), &jsThis, (void**)&method); - dynamicArgs.resize(argcRetry); - actualArgc = argcRetry; - callArgs = dynamicArgs.data(); - } else if (!method->overloads.empty()) { - dynamicArgs.assign(stackArgs, stackArgs + actualArgc); + if (!method->overloads.empty() && actualArgc > 0 && rawCallArgs != nullptr) { + dynamicArgs.assign(rawCallArgs, rawCallArgs + actualArgc); callArgs = dynamicArgs.data(); } @@ -1475,6 +1503,20 @@ explicit CifReturnStorage(Cif* cif) { method->cif = selectedCif; } + if (selectedCif != nullptr && !selectedCif->isVariadic && + selectedCif->argc != actualArgc) { + Method runtimeMethod = receiverIsClass + ? class_getClassMethod(receiverClass, selectedMethod->selector) + : class_getInstanceMethod(receiverClass, selectedMethod->selector); + if (runtimeMethod != nullptr) { + Cif* runtimeCif = method->bridgeState->getMethodCif(env, runtimeMethod); + if (runtimeCif != nullptr && runtimeCif->argc == actualArgc) { + selectedCif = runtimeCif; + method->cif = selectedCif; + } + } + } + if (!method->overloads.empty()) { struct Candidate { MethodDescriptor* descriptor; @@ -1547,6 +1589,11 @@ explicit CifReturnStorage(Cif* cif) { return nullptr; } + std::optional roundTripCacheFrame; + if (generatedDispatchNeedsRoundTripCacheFrame(cif)) { + roundTripCacheFrame.emplace(env, method->bridgeState); + } + CifReturnStorage rvalueStorage(cif); if (!rvalueStorage.valid()) { napi_throw_error(env, "NativeScriptException", @@ -1617,6 +1664,15 @@ explicit CifReturnStorage(Cif* cif) { napi_get_named_property(env, jsThis, "constructor", &constructor); } id obj = *((id*)nativeResult); + if (obj != nil) { + ObjCBridgeState* state = ObjCBridgeState::InstanceData(env); + if (state != nullptr) { + if (napi_value cached = state->findCachedObjectWrapper(env, obj); + cached != nullptr) { + return cached; + } + } + } return method->bridgeState->getObject(env, obj, constructor, method->returnOwned ? kOwnedObject : kUnownedObject); } @@ -1636,7 +1692,8 @@ explicit CifReturnStorage(Cif* cif) { } } - return cif->returnType->toJS(env, nativeResult, method->returnOwned ? kReturnOwned : 0); + return cif->returnType->toJS(env, nativeResult, + method->returnOwned ? kReturnOwned : 0); }; bool usesBlockFallback = false; @@ -1654,7 +1711,7 @@ explicit CifReturnStorage(Cif* cif) { } } - if (!hasImplicitNSErrorOutArg && !usesBlockFallback) { + if (!isNSErrorOutMethod && !usesBlockFallback) { bool didDirectInvoke = false; if (!tryObjCNapiDispatch(env, cif, self, receiverIsClass, selectedSelector, selectedMethod, selectedMethod->dispatchFlags, invocationArgs, rvalue, @@ -1690,7 +1747,8 @@ explicit CifReturnStorage(Cif* cif) { const char* blockEncoding = blockEncodingForSelector(selectedSelectorName, i); if (hasImplicitNSErrorOutArg && i == cif->argc - 1) { - *((NSError***)avalues[i + 2]) = &implicitNSError; + NSError** implicitNSErrorOutArg = &implicitNSError; + *reinterpret_cast(avalues[i + 2]) = implicitNSErrorOutArg; continue; } @@ -1749,10 +1807,20 @@ NativeScriptException nativeScriptException(errorMessage != nullptr ? errorMessa napi_value ObjCClassMember::jsGetter(napi_env env, napi_callback_info cbinfo) { napi_value jsThis; - ObjCClassMember* method; + ObjCClassMember* method = nullptr; napi_get_cb_info(env, cbinfo, nullptr, nullptr, &jsThis, (void**)&method); + return jsGetterDirect(env, method, jsThis); +} + +napi_value ObjCClassMember::jsGetterDirect(napi_env env, ObjCClassMember* method, + napi_value jsThis) { + if (method == nullptr) { + napi_throw_error(env, "NativeScriptException", "Missing Objective-C getter metadata."); + return nullptr; + } + id self = assertSelf(env, jsThis, method); if (self == nullptr) { @@ -1806,6 +1874,15 @@ NativeScriptException nativeScriptException(errorMessage != nullptr ? errorMessa napi_get_named_property(env, jsThis, "constructor", &constructor); } id obj = *((id*)rvalue); + if (obj != nil) { + ObjCBridgeState* state = ObjCBridgeState::InstanceData(env); + if (state != nullptr) { + if (napi_value cached = state->findCachedObjectWrapper(env, obj); + cached != nullptr) { + return cached; + } + } + } return method->bridgeState->getObject(env, obj, constructor, method->returnOwned ? kOwnedObject : kUnownedObject); } @@ -1814,6 +1891,10 @@ NativeScriptException nativeScriptException(errorMessage != nullptr ? errorMessa } napi_value ObjCClassMember::jsReadOnlySetter(napi_env env, napi_callback_info cbinfo) { + return jsReadOnlySetterDirect(env); +} + +napi_value ObjCClassMember::jsReadOnlySetterDirect(napi_env env) { napi_throw_error(env, nullptr, "Attempted to assign to readonly property."); return nullptr; } @@ -1821,10 +1902,20 @@ NativeScriptException nativeScriptException(errorMessage != nullptr ? errorMessa napi_value ObjCClassMember::jsSetter(napi_env env, napi_callback_info cbinfo) { napi_value jsThis, argv; size_t argc = 1; - ObjCClassMember* method; + ObjCClassMember* method = nullptr; napi_get_cb_info(env, cbinfo, &argc, &argv, &jsThis, (void**)&method); + return jsSetterDirect(env, method, jsThis, argv); +} + +napi_value ObjCClassMember::jsSetterDirect(napi_env env, ObjCClassMember* method, + napi_value jsThis, napi_value value) { + if (method == nullptr) { + napi_throw_error(env, "NativeScriptException", "Missing Objective-C setter metadata."); + return nullptr; + } + id self = assertSelf(env, jsThis, method); if (self == nullptr) { @@ -1833,16 +1924,19 @@ NativeScriptException nativeScriptException(errorMessage != nullptr ? errorMessa const bool receiverIsClass = object_isClass(self); - RoundTripCacheFrameGuard roundTripCacheFrame(env, method->bridgeState); - Cif* cif = method->setterCif; if (cif == nullptr) { cif = method->setterCif = method->bridgeState->getMethodCif(env, method->setter.signatureOffset); } + std::optional roundTripCacheFrame; + if (generatedDispatchNeedsRoundTripCacheFrame(cif)) { + roundTripCacheFrame.emplace(env, method->bridgeState); + } + if (cif->argc > 0) { - cif->argv[0] = argv; + cif->argv[0] = value; } bool didDirectInvoke = false; @@ -1867,7 +1961,7 @@ NativeScriptException nativeScriptException(errorMessage != nullptr ? errorMessa void* rvalue = nullptr; bool shouldFree = false; - cif->argTypes[0]->toNative(env, argv, avalues[2], &shouldFree, &shouldFree); + cif->argTypes[0]->toNative(env, value, avalues[2], &shouldFree, &shouldFree); if (!objcNativeCall(env, cif, self, receiverIsClass, &method->setter, method->setter.dispatchFlags, avalues, rvalue)) { diff --git a/NativeScript/ffi/Closure.h b/NativeScript/ffi/napi/Closure.h similarity index 100% rename from NativeScript/ffi/Closure.h rename to NativeScript/ffi/napi/Closure.h diff --git a/NativeScript/ffi/Closure.mm b/NativeScript/ffi/napi/Closure.mm similarity index 98% rename from NativeScript/ffi/Closure.mm rename to NativeScript/ffi/napi/Closure.mm index aea75385..ab90659c 100644 --- a/NativeScript/ffi/Closure.mm +++ b/NativeScript/ffi/napi/Closure.mm @@ -1,11 +1,12 @@ #include "Closure.h" #include "AutoreleasePool.h" +#include "CallbackThreading.h" #include "Metadata.h" #include "MetadataReader.h" #include "ObjCBridge.h" #include "TypeConv.h" #include "Util.h" -#include "ffi/NativeScriptException.h" +#include "runtime/NativeScriptException.h" #include "js_native_api.h" #include "js_native_api_types.h" #ifdef ENABLE_JS_RUNTIME @@ -262,7 +263,7 @@ void JSFunctionCallback(ffi_cif* cif, void* ret, void* args[], void* data) { napi_env env = closure->env; #ifdef ENABLE_JS_RUNTIME - NapiScope scope(env); + NativeCallbackScope scope(env); #endif napi_value func = get_ref_value(env, closure->func); @@ -314,7 +315,9 @@ void JSFunctionCallback(ffi_cif* cif, void* ret, void* args[], void* data) { JSCallbackInner(closure, func, thisArg, argv, ctx->cif->nargs - 1, nullptr, ctx->ret); #ifdef TARGET_ENGINE_HERMES - js_execute_pending_jobs(env); + if (!isNativeCallerThreadCallbackActive()) { + js_execute_pending_jobs(env); + } #endif if (ctx->useCondvar) { { @@ -347,9 +350,9 @@ void JSBlockCallback(ffi_cif* cif, void* ret, void* args[], void* data) { ctx.done = false; ctx.useCondvar = false; - if (currentThreadId == closure->jsThreadId) { + if (currentThreadId == closure->jsThreadId || shouldInvokeCallbackOnNativeCallerThread()) { #ifdef ENABLE_JS_RUNTIME - NapiScope scope(env); + NativeCallbackScope scope(env); #endif Closure::callBlockFromMainThread(env, get_ref_value(env, closure->func), closure, &ctx); } else { diff --git a/NativeScript/ffi/Enum.h b/NativeScript/ffi/napi/Enum.h similarity index 100% rename from NativeScript/ffi/Enum.h rename to NativeScript/ffi/napi/Enum.h diff --git a/NativeScript/ffi/Enum.mm b/NativeScript/ffi/napi/Enum.mm similarity index 100% rename from NativeScript/ffi/Enum.mm rename to NativeScript/ffi/napi/Enum.mm diff --git a/NativeScript/ffi/InlineFunctions.h b/NativeScript/ffi/napi/InlineFunctions.h similarity index 100% rename from NativeScript/ffi/InlineFunctions.h rename to NativeScript/ffi/napi/InlineFunctions.h diff --git a/NativeScript/ffi/InlineFunctions.mm b/NativeScript/ffi/napi/InlineFunctions.mm similarity index 100% rename from NativeScript/ffi/InlineFunctions.mm rename to NativeScript/ffi/napi/InlineFunctions.mm diff --git a/NativeScript/ffi/Interop.h b/NativeScript/ffi/napi/Interop.h similarity index 100% rename from NativeScript/ffi/Interop.h rename to NativeScript/ffi/napi/Interop.h diff --git a/NativeScript/ffi/Interop.mm b/NativeScript/ffi/napi/Interop.mm similarity index 99% rename from NativeScript/ffi/Interop.mm rename to NativeScript/ffi/napi/Interop.mm index 6e5ca5ef..9ff39883 100644 --- a/NativeScript/ffi/Interop.mm +++ b/NativeScript/ffi/napi/Interop.mm @@ -424,11 +424,19 @@ inline bool unwrapKnownNativeHandle(napi_env env, napi_value value, void** out) napi_value nativePointerValue; if (napi_get_named_property(env, value, kNativePointerProperty, &nativePointerValue) == napi_ok) { - void* nativePointer = nullptr; - if (napi_get_value_external(env, nativePointerValue, &nativePointer) == napi_ok && - nativePointer != nullptr) { - *out = nativePointer; - return true; + if (Pointer::isInstance(env, nativePointerValue)) { + Pointer* pointer = Pointer::unwrap(env, nativePointerValue); + if (pointer != nullptr && pointer->data != nullptr) { + *out = pointer->data; + return true; + } + } else { + void* nativePointer = nullptr; + if (napi_get_value_external(env, nativePointerValue, &nativePointer) == napi_ok && + nativePointer != nullptr) { + *out = nativePointer; + return true; + } } } } @@ -582,7 +590,7 @@ napi_value __extends(napi_env env, napi_callback_info info) { if (superClassNative != nullptr) { ClassBuilder* builder = new ClassBuilder(env, constructor); ObjCBridgeState* bridgeState = ObjCBridgeState::InstanceData(env); - bridgeState->classesByPointer[builder->nativeClass] = builder; + bridgeState->registerRuntimeClass(builder, builder->nativeClass); } return nullptr; diff --git a/NativeScript/ffi/JSObject.h b/NativeScript/ffi/napi/JSObject.h similarity index 100% rename from NativeScript/ffi/JSObject.h rename to NativeScript/ffi/napi/JSObject.h diff --git a/NativeScript/ffi/JSObject.mm b/NativeScript/ffi/napi/JSObject.mm similarity index 100% rename from NativeScript/ffi/JSObject.mm rename to NativeScript/ffi/napi/JSObject.mm diff --git a/NativeScript/ffi/ObjCBridge.h b/NativeScript/ffi/napi/ObjCBridge.h similarity index 70% rename from NativeScript/ffi/ObjCBridge.h rename to NativeScript/ffi/napi/ObjCBridge.h index d2e81b2a..6e7eb19a 100644 --- a/NativeScript/ffi/ObjCBridge.h +++ b/NativeScript/ffi/napi/ObjCBridge.h @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -41,6 +42,17 @@ struct JSObjectFinalizerContext { napi_ref ref; }; +struct HandleObjectRef { + napi_ref ref = nullptr; + bool ownsRef = false; +}; + +struct RecentObjectWrapperRef { + uintptr_t objectKey = 0; + uintptr_t objectClassKey = 0; + napi_ref ref = nullptr; +}; + void finalize_objc_object(napi_env /*env*/, void* data, void* hint); bool IsBridgeStateLive(const ObjCBridgeState* bridgeState, uint64_t token) noexcept; @@ -97,6 +109,48 @@ class ObjCBridgeState { #endif } + inline void registerRuntimeClass(ObjCClass* bridgedClass, + Class runtimeClass) { + if (bridgedClass == nullptr || runtimeClass == nil) { + return; + } + + bridgedClass->nativeClass = runtimeClass; + classesByPointer[runtimeClass] = bridgedClass; + nativeObjectsByBridgeWrapper[bridgedClass] = (id)runtimeClass; + if (bridgedClass->metadataOffset != MD_SECTION_OFFSET_NULL) { + mdClassesByPointer[runtimeClass] = bridgedClass->metadataOffset; + } + } + + inline void registerProtocolMetadata(Protocol* runtimeProtocol, + MDSectionOffset metadataOffset) { + if (runtimeProtocol == nil || metadataOffset == MD_SECTION_OFFSET_NULL) { + return; + } + + mdProtocolsByPointer[runtimeProtocol] = metadataOffset; + } + + inline void registerRuntimeProtocol(ObjCProtocol* bridgedProtocol, + Protocol* runtimeProtocol) { + if (bridgedProtocol == nullptr || runtimeProtocol == nil) { + return; + } + + nativeObjectsByBridgeWrapper[bridgedProtocol] = (id)runtimeProtocol; + registerProtocolMetadata(runtimeProtocol, bridgedProtocol->metadataOffset); + } + + inline id nativeObjectForBridgeWrapper(void* wrapped) const { + if (wrapped == nullptr) { + return nil; + } + + auto cached = nativeObjectsByBridgeWrapper.find(wrapped); + return cached != nativeObjectsByBridgeWrapper.end() ? cached->second : nil; + } + void registerVarGlobals(napi_env env, napi_value global); void registerEnumGlobals(napi_env env, napi_value global); void registerStructGlobals(napi_env env, napi_value global); @@ -124,6 +178,13 @@ class ObjCBridgeState { MDSectionOffset classOffset = 0, std::vector* protocolOffsets = nullptr); napi_value findCachedObjectWrapper(napi_env env, id object); + inline void deleteOwnedHandleObjectRef(napi_env env, HandleObjectRef& entry) { + if (entry.ownsRef && env != nullptr && entry.ref != nullptr) { + napi_delete_reference(env, entry.ref); + } + entry.ref = nullptr; + entry.ownsRef = false; + } inline void cacheHandleObject(napi_env env, void* handle, napi_value value) { if (handle == nullptr || value == nullptr) { return; @@ -132,7 +193,7 @@ class ObjCBridgeState { uintptr_t handleKey = NormalizeHandleKey(handle); auto it = handleObjectRefs.find(handleKey); if (it != handleObjectRefs.end()) { - auto existing = get_ref_value(env, it->second); + auto existing = get_ref_value(env, it->second.ref); if (existing != nullptr) { bool isSameValue = false; if (napi_strict_equals(env, existing, value, &isSameValue) == napi_ok && @@ -141,13 +202,35 @@ class ObjCBridgeState { } } - napi_delete_reference(env, it->second); + deleteOwnedHandleObjectRef(env, it->second); handleObjectRefs.erase(it); + bumpHandleObjectRefsGeneration(); } napi_ref ref = nullptr; napi_create_reference(env, value, 0, &ref); - handleObjectRefs[handleKey] = ref; + handleObjectRefs[handleKey] = HandleObjectRef{ref, true}; + bumpHandleObjectRefsGeneration(); + } + inline void cacheHandleObjectRef(napi_env env, void* handle, napi_ref ref) { + if (handle == nullptr || ref == nullptr) { + return; + } + + uintptr_t handleKey = NormalizeHandleKey(handle); + auto it = handleObjectRefs.find(handleKey); + if (it != handleObjectRefs.end()) { + if (it->second.ref == ref) { + return; + } + + deleteOwnedHandleObjectRef(env, it->second); + handleObjectRefs.erase(it); + bumpHandleObjectRefsGeneration(); + } + + handleObjectRefs[handleKey] = HandleObjectRef{ref, false}; + bumpHandleObjectRefsGeneration(); } inline napi_value getCachedHandleObject(napi_env env, void* handle) { if (handle == nullptr) { @@ -155,19 +238,192 @@ class ObjCBridgeState { } uintptr_t handleKey = NormalizeHandleKey(handle); + const uint64_t generation = currentHandleObjectRefsGeneration(); + + struct LastHandleObjectRef { + const ObjCBridgeState* bridgeState = nullptr; + napi_env env = nullptr; + uintptr_t handleKey = 0; + napi_ref ref = nullptr; + uint64_t generation = 0; + }; + + static thread_local LastHandleObjectRef lastHandleObjectRef; + if (lastHandleObjectRef.bridgeState == this && + lastHandleObjectRef.env == env && + lastHandleObjectRef.handleKey == handleKey && + lastHandleObjectRef.ref != nullptr && + lastHandleObjectRef.generation == generation) { + napi_value value = get_ref_value(env, lastHandleObjectRef.ref); + if (value != nullptr) { + return value; + } + } + + static thread_local LastHandleObjectRef recentHandleObjectRefs[8]; + static thread_local unsigned int nextRecentHandleObjectRefSlot = 0; + for (const auto& entry : recentHandleObjectRefs) { + if (entry.bridgeState != this || + entry.env != env || + entry.handleKey != handleKey || + entry.ref == nullptr || + entry.generation != generation) { + continue; + } + + napi_value value = get_ref_value(env, entry.ref); + if (value != nullptr) { + lastHandleObjectRef = entry; + return value; + } + break; + } + auto it = handleObjectRefs.find(handleKey); if (it == handleObjectRefs.end()) { return nullptr; } - auto value = get_ref_value(env, it->second); + auto value = get_ref_value(env, it->second.ref); if (value == nullptr) { - napi_delete_reference(env, it->second); + deleteOwnedHandleObjectRef(env, it->second); handleObjectRefs.erase(it); + bumpHandleObjectRefsGeneration(); + if (lastHandleObjectRef.bridgeState == this && + lastHandleObjectRef.handleKey == handleKey) { + lastHandleObjectRef = {}; + } + return nullptr; } + lastHandleObjectRef = LastHandleObjectRef{ + .bridgeState = this, + .env = env, + .handleKey = handleKey, + .ref = it->second.ref, + .generation = generation, + }; + recentHandleObjectRefs[nextRecentHandleObjectRefSlot++ & 7] = + lastHandleObjectRef; return value; } + inline bool ownsCachedHandleObjectRef(void* handle) const noexcept { + if (handle == nullptr) { + return false; + } + + auto it = handleObjectRefs.find(NormalizeHandleKey(handle)); + return it != handleObjectRefs.end() && it->second.ownsRef; + } + inline void removeCachedHandleObject(napi_env env, void* handle) noexcept { + if (handle == nullptr) { + return; + } + + uintptr_t handleKey = NormalizeHandleKey(handle); + auto it = handleObjectRefs.find(handleKey); + if (it == handleObjectRefs.end()) { + return; + } + + deleteOwnedHandleObjectRef(env, it->second); + handleObjectRefs.erase(it); + bumpHandleObjectRefsGeneration(); + } + inline void deleteRecentObjectWrapperRef(napi_env env, + RecentObjectWrapperRef& entry) { + if (env != nullptr && entry.ref != nullptr) { + napi_delete_reference(env, entry.ref); + } + entry = {}; + } + inline void cacheRecentObjectWrapper(napi_env env, id object, + napi_value value) { + if (env == nullptr || object == nil || value == nullptr) { + return; + } + + const uintptr_t objectKey = NormalizeHandleKey((void*)object); + const uintptr_t objectClassKey = NormalizeHandleKey((void*)object_getClass(object)); + for (auto& entry : recentObjectWrappers) { + if (entry.objectKey != objectKey || entry.objectClassKey != objectClassKey) { + continue; + } + + napi_value existing = get_ref_value(env, entry.ref); + if (existing != nullptr) { + bool isSameValue = false; + if (napi_strict_equals(env, existing, value, &isSameValue) == napi_ok && + isSameValue) { + return; + } + } + + deleteRecentObjectWrapperRef(env, entry); + napi_create_reference(env, value, 1, &entry.ref); + entry.objectKey = objectKey; + entry.objectClassKey = objectClassKey; + return; + } + + RecentObjectWrapperRef entry{ + .objectKey = objectKey, + .objectClassKey = objectClassKey, + .ref = nullptr, + }; + napi_create_reference(env, value, 1, &entry.ref); + + static constexpr size_t kRecentObjectWrapperLimit = 16; + if (recentObjectWrappers.size() < kRecentObjectWrapperLimit) { + recentObjectWrappers.push_back(entry); + return; + } + + RecentObjectWrapperRef& replaced = + recentObjectWrappers[nextRecentObjectWrapperSlot++ % kRecentObjectWrapperLimit]; + deleteRecentObjectWrapperRef(env, replaced); + replaced = entry; + } + inline napi_value getRecentObjectWrapper(napi_env env, id object) { + if (env == nullptr || object == nil) { + return nullptr; + } + + const uintptr_t objectKey = NormalizeHandleKey((void*)object); + const uintptr_t objectClassKey = NormalizeHandleKey((void*)object_getClass(object)); + for (auto it = recentObjectWrappers.begin(); it != recentObjectWrappers.end();) { + if (it->objectKey != objectKey || it->objectClassKey != objectClassKey) { + ++it; + continue; + } + + napi_value value = get_ref_value(env, it->ref); + if (value != nullptr) { + return value; + } + + deleteRecentObjectWrapperRef(env, *it); + it = recentObjectWrappers.erase(it); + } + + return nullptr; + } + inline void removeRecentObjectWrapper(napi_env env, id object) noexcept { + if (env == nullptr || object == nil) { + return; + } + + const uintptr_t objectKey = NormalizeHandleKey((void*)object); + const uintptr_t objectClassKey = NormalizeHandleKey((void*)object_getClass(object)); + for (auto it = recentObjectWrappers.begin(); it != recentObjectWrappers.end();) { + if (it->objectKey == objectKey && it->objectClassKey == objectClassKey) { + deleteRecentObjectWrapperRef(env, *it); + it = recentObjectWrappers.erase(it); + } else { + ++it; + } + } + } void unregisterObject(id object) noexcept; bool unregisterObjectIfRefMatches(id object, napi_ref ref) noexcept; @@ -182,10 +438,10 @@ class ObjCBridgeState { } uintptr_t objectKey = NormalizeHandleKey((void*)object); - Class objectClass = object_getClass(object); + uintptr_t objectClassKey = NormalizeHandleKey((void*)object_getClass(object)); for (const auto& entry : objectRefs) { if (NormalizeHandleKey((void*)entry.first) == objectKey && - object_getClass(entry.first) == objectClass) { + NormalizeHandleKey((void*)object_getClass(entry.first)) == objectClassKey) { return true; } } @@ -193,6 +449,14 @@ class ObjCBridgeState { return false; } + inline uint64_t currentObjectRefsGeneration() const noexcept { + return objectRefsGeneration.load(std::memory_order_relaxed); + } + + inline uint64_t currentHandleObjectRefsGeneration() const noexcept { + return handleObjectRefsGeneration.load(std::memory_order_relaxed); + } + inline void beginRoundTripCacheFrame(napi_env /*env*/) { roundTripCacheFrames.emplace_back(); } @@ -412,19 +676,6 @@ class ObjCBridgeState { return name; }; - auto registerResolvedRuntimeClass = [&](ObjCClass* bridgedClass, - Class runtimeClass) { - if (bridgedClass == nullptr || runtimeClass == nil) { - return; - } - - bridgedClass->nativeClass = runtimeClass; - classesByPointer[runtimeClass] = bridgedClass; - if (bridgedClass->metadataOffset != MD_SECTION_OFFSET_NULL) { - mdClassesByPointer[runtimeClass] = bridgedClass->metadataOffset; - } - }; - auto matchesConstructor = [&](ObjCClass* bridgedClass, ObjCClass** unresolvedMatch) -> bool { if (bridgedClass == nullptr || bridgedClass->constructor == nullptr) { @@ -485,7 +736,7 @@ class ObjCBridgeState { Class runtimeClass = objc_lookUpClass(candidateName.c_str()); if (runtimeClass != nil) { if (unresolvedConstructorMatch != nullptr) { - registerResolvedRuntimeClass(unresolvedConstructorMatch, runtimeClass); + registerRuntimeClass(unresolvedConstructorMatch, runtimeClass); } *out = runtimeClass; return true; @@ -689,7 +940,9 @@ class ObjCBridgeState { std::thread::id jsThreadId = std::this_thread::get_id(); CFRunLoopRef jsRunLoop = CFRunLoopGetCurrent(); std::unordered_map objectRefs; - std::unordered_map handleObjectRefs; + std::unordered_map handleObjectRefs; + std::vector recentObjectWrappers; + size_t nextRecentObjectWrapperSlot = 0; napi_ref pointerClass = nullptr; napi_ref referenceClass = nullptr; @@ -703,6 +956,7 @@ class ObjCBridgeState { std::unordered_map classesByPointer; std::unordered_map mdClassesByPointer; std::unordered_map mdProtocolsByPointer; + std::unordered_map nativeObjectsByBridgeWrapper; std::unordered_map constructorsByPointer; std::unordered_map cifs; @@ -720,9 +974,18 @@ class ObjCBridgeState { MDMetadataReader* metadata; private: + inline void bumpObjectRefsGeneration() noexcept { + objectRefsGeneration.fetch_add(1, std::memory_order_relaxed); + } + + inline void bumpHandleObjectRefsGeneration() noexcept { + handleObjectRefsGeneration.fetch_add(1, std::memory_order_relaxed); + } + inline void storeObjectRef(id object, napi_ref ref) noexcept { std::lock_guard lock(objectRefsMutex); objectRefs[object] = ref; + bumpObjectRefsGeneration(); } inline napi_value getNormalizedObjectRef(napi_env env, id object) const { @@ -734,10 +997,10 @@ class ObjCBridgeState { } uintptr_t objectKey = NormalizeHandleKey((void*)object); - Class objectClass = object_getClass(object); + uintptr_t objectClassKey = NormalizeHandleKey((void*)object_getClass(object)); for (const auto& entry : objectRefs) { if (NormalizeHandleKey((void*)entry.first) != objectKey || - object_getClass(entry.first) != objectClass) { + NormalizeHandleKey((void*)object_getClass(entry.first)) != objectClassKey) { continue; } @@ -762,12 +1025,15 @@ class ObjCBridgeState { napi_ref ref = exact->second; objectRefs.erase(exact); + bumpObjectRefsGeneration(); return ref; } std::unordered_map structInfoCache; std::vector> roundTripCacheFrames; std::unordered_map recentRoundTripCache; + std::atomic objectRefsGeneration{1}; + std::atomic handleObjectRefsGeneration{1}; mutable std::mutex objectRefsMutex; void* trackedObjectLiveness = nullptr; void* objc_autoreleasePool; diff --git a/NativeScript/ffi/ObjCBridge.mm b/NativeScript/ffi/napi/ObjCBridge.mm similarity index 97% rename from NativeScript/ffi/ObjCBridge.mm rename to NativeScript/ffi/napi/ObjCBridge.mm index bb304ea7..0c7ebd76 100644 --- a/NativeScript/ffi/ObjCBridge.mm +++ b/NativeScript/ffi/napi/ObjCBridge.mm @@ -858,10 +858,17 @@ void registerLegacyCompatGlobals(napi_env env, napi_value global, ObjCBridgeStat recentRoundTripCache.clear(); for (auto& entry : handleObjectRefs) { - deleteRef(entry.second); + if (entry.second.ownsRef) { + deleteRef(entry.second.ref); + } } handleObjectRefs.clear(); + for (auto& entry : recentObjectWrappers) { + deleteRef(entry.ref); + } + recentObjectWrappers.clear(); + std::unordered_set classAndProtocolConstructorRefs; classAndProtocolConstructorRefs.reserve(classes.size() + protocols.size()); for (const auto& pair : classes) { @@ -949,13 +956,19 @@ void registerLegacyCompatGlobals(napi_env env, napi_value global, ObjCBridgeStat napi_value ObjCBridgeState::proxyNativeObject(napi_env env, napi_value object, id nativeObject) { NAPI_PREAMBLE - napi_value factory = get_ref_value(env, createNativeProxy); - napi_value transferOwnershipFunc = get_ref_value(env, this->transferOwnershipToNative); - napi_value result, global; - napi_value args[3] = {object, nullptr, transferOwnershipFunc}; - napi_get_boolean(env, [nativeObject isKindOfClass:NSArray.class], &args[1]); - napi_get_global(env, &global); - napi_call_function(env, global, factory, 3, args, &result); + napi_value result = object; + const bool nativeIsArray = [nativeObject isKindOfClass:NSArray.class]; + bool shouldProxyArray = nativeIsArray; + if (shouldProxyArray) { + napi_value factory = get_ref_value(env, createNativeProxy); + napi_value transferOwnershipFunc = get_ref_value(env, this->transferOwnershipToNative); + napi_value global; + napi_value args[3] = {object, nullptr, transferOwnershipFunc}; + napi_get_boolean(env, true, &args[1]); + napi_get_global(env, &global); + napi_call_function(env, global, factory, 3, args, &result); + } + napi_value nativePointer = Pointer::create(env, nativeObject); if (nativePointer != nullptr) { napi_set_named_property(env, result, kNativePointerProperty, nativePointer); @@ -978,6 +991,8 @@ void registerLegacyCompatGlobals(napi_env env, napi_value global, ObjCBridgeStat finalizerContext->ref = ref; storeObjectRef(nativeObject, ref); + cacheHandleObjectRef(env, nativeObject, ref); + cacheRecentObjectWrapper(env, nativeObject, result); attachObjectLifecycleAssociation(env, nativeObject); trackObject(nativeObject); diff --git a/NativeScript/ffi/Object.h b/NativeScript/ffi/napi/Object.h similarity index 78% rename from NativeScript/ffi/Object.h rename to NativeScript/ffi/napi/Object.h index 2290468f..3174bb91 100644 --- a/NativeScript/ffi/Object.h +++ b/NativeScript/ffi/napi/Object.h @@ -7,6 +7,7 @@ namespace nativescript { void initProxyFactory(napi_env env, ObjCBridgeState* bridgeState); void attachObjectLifecycleAssociation(napi_env env, id object); +void transferOwnershipToNative(napi_env env, napi_value value, id object); } // namespace nativescript diff --git a/NativeScript/ffi/Object.mm b/NativeScript/ffi/napi/Object.mm similarity index 76% rename from NativeScript/ffi/Object.mm rename to NativeScript/ffi/napi/Object.mm index 10f043c9..5283280e 100644 --- a/NativeScript/ffi/Object.mm +++ b/NativeScript/ffi/napi/Object.mm @@ -128,6 +128,14 @@ napi_value JS_transferOwnershipToNative(napi_env env, napi_callback_info cbinfo) napi_value findConstructorForObject(napi_env env, ObjCBridgeState* bridgeState, id object, Class cls = nil); +void transferOwnershipToNative(napi_env env, napi_value value, id object) { + if (env == nullptr || value == nullptr || object == nil) { + return; + } + + [JSWrapperObjectAssociation transferOwnership:env of:value toNative:object]; +} + namespace { constexpr const char* kNativePointerProperty = "__ns_native_ptr"; @@ -179,6 +187,41 @@ napi_value findConstructorForClassObject(napi_env env, ObjCBridgeState* bridgeSt const char* nativeObjectProxySource = R"( (function (object, isArray, transferOwnershipToNative) { let isTransfered = false; + const boundMethods = Object.create(null); + let objectAtIndexMethod; + let addObjectMethod; + let removeObjectAtIndexMethod; + let setObjectAtIndexedSubscriptMethod; + + function bindTargetMethod(target, name) { + const cached = boundMethods[name]; + if (cached !== undefined) { + return cached; + } + + const value = target[name]; + if (typeof value !== "function") { + return value; + } + + if (value.__ns_proxy_bound === true) { + boundMethods[name] = value; + return value; + } + + const wrapper = value.bind(target); + Object.defineProperty(wrapper, "__ns_proxy_bound", { value: true }); + boundMethods[name] = wrapper; + try { + Object.defineProperty(target, name, { + value: wrapper, + configurable: true, + writable: true + }); + } catch (_) { + } + return wrapper; + } return new Proxy(object, { get (target, name, receiver) { @@ -186,47 +229,102 @@ napi_value findConstructorForClassObject(napi_env env, ObjCBridgeState* bridgeSt return target.class().superclass(); } - if (name in target) { - const value = target[name]; + if (isArray && name === "count") { + return target.count; + } + + if (isArray) { + switch (name) { + case "objectAtIndex": + return objectAtIndexMethod !== undefined + ? objectAtIndexMethod + : (objectAtIndexMethod = bindTargetMethod(target, name)); + case "addObject": + return addObjectMethod !== undefined + ? addObjectMethod + : (addObjectMethod = bindTargetMethod(target, name)); + case "removeObjectAtIndex": + return removeObjectAtIndexMethod !== undefined + ? removeObjectAtIndexMethod + : (removeObjectAtIndexMethod = bindTargetMethod(target, name)); + case "setObjectAtIndexedSubscript": + return setObjectAtIndexedSubscriptMethod !== undefined + ? setObjectAtIndexedSubscriptMethod + : (setObjectAtIndexedSubscriptMethod = bindTargetMethod(target, name)); + } + } + + if (typeof name === "string") { + const boundMethod = boundMethods[name]; + if (boundMethod !== undefined) { + return boundMethod; + } + } + + const value = target[name]; + if (value !== undefined || name in target) { if (typeof value === "function" && name !== "constructor") { + if (value.__ns_proxy_bound === true) { + return value; + } + + let wrapper; if ((name === "isKindOfClass" || name === "isMemberOfClass")) { - return function (cls, ...args) { + wrapper = function (cls, a1, a2, a3) { let resolvedClass = cls; + const resolvedClassType = typeof resolvedClass; if (resolvedClass != null && - (typeof resolvedClass === "object" || typeof resolvedClass === "function")) { + (resolvedClassType === "object" || resolvedClassType === "function")) { try { const runtimeName = typeof NSStringFromClass === "function" ? NSStringFromClass(resolvedClass) : null; - if (typeof runtimeName === "string" && runtimeName.length > 0) { - const registry = globalThis.__nsConstructorsByObjCClassName; - if (registry && registry[runtimeName]) { - resolvedClass = registry[runtimeName]; - } else if (typeof globalThis[runtimeName] !== "undefined") { - resolvedClass = globalThis[runtimeName]; - } - } + if (typeof runtimeName === "string" && runtimeName.length > 0) { + const registry = globalThis.__nsConstructorsByObjCClassName; + if (registry && registry[runtimeName]) { + resolvedClass = registry[runtimeName]; + } else if (typeof globalThis[runtimeName] !== "undefined") { + resolvedClass = globalThis[runtimeName]; + } + } } catch (_) { } } - value.__ns_bound_receiver = receiver; - try { - return Reflect.apply(value, receiver, [resolvedClass, ...args]); - } finally { - value.__ns_bound_receiver = undefined; + switch (arguments.length) { + case 0: + case 1: + return value.call(target, resolvedClass); + case 2: + return value.call(target, resolvedClass, a1); + case 3: + return value.call(target, resolvedClass, a1, a2); + case 4: + return value.call(target, resolvedClass, a1, a2, a3); + default: { + const args = Array.prototype.slice.call(arguments); + args[0] = resolvedClass; + return Reflect.apply(value, target, args); + } } }; + } else { + wrapper = value.bind(target); } - return function (...args) { - value.__ns_bound_receiver = receiver; - try { - return Reflect.apply(value, receiver, args); - } finally { - value.__ns_bound_receiver = undefined; - } - }; + Object.defineProperty(wrapper, "__ns_proxy_bound", { value: true }); + if (typeof name === "string") { + boundMethods[name] = wrapper; + } + try { + Object.defineProperty(target, name, { + value: wrapper, + configurable: true, + writable: true + }); + } catch (_) { + } + return wrapper; } return value; } @@ -252,6 +350,27 @@ napi_value findConstructorForClassObject(napi_env env, ObjCBridgeState* bridgeSt return true; } + if (typeof name === "string" && boundMethods[name] !== undefined) { + delete boundMethods[name]; + } + + if (isArray) { + switch (name) { + case "objectAtIndex": + objectAtIndexMethod = undefined; + break; + case "addObject": + addObjectMethod = undefined; + break; + case "removeObjectAtIndex": + removeObjectAtIndexMethod = undefined; + break; + case "setObjectAtIndexedSubscript": + setObjectAtIndexedSubscriptMethod = undefined; + break; + } + } + if (isArray) { const index = Number(name); if (!isNaN(index)) { @@ -437,12 +556,39 @@ void finalize_objc_object(napi_env env, void* data, void* hint) { return roundTrip; } + if (napi_value recentWrapper = getRecentObjectWrapper(env, obj); recentWrapper != nullptr) { + return recentWrapper; + } + if (napi_value handleCached = getCachedHandleObject(env, (void*)obj); handleCached != nullptr) { + bool isRawHandleRoundTrip = ownsCachedHandleObjectRef((void*)obj); void* wrapped = nullptr; if (napi_unwrap(env, handleCached, &wrapped) == napi_ok && NormalizeHandleKey(wrapped) == NormalizeHandleKey((void*)obj)) { return handleCached; } + + bool hasNativePointer = false; + if (napi_has_named_property(env, handleCached, kNativePointerProperty, &hasNativePointer) == + napi_ok && + hasNativePointer) { + napi_value nativePointerValue = nullptr; + if (napi_get_named_property(env, handleCached, kNativePointerProperty, &nativePointerValue) == + napi_ok && + Pointer::isInstance(env, nativePointerValue)) { + Pointer* pointer = Pointer::unwrap(env, nativePointerValue); + if (pointer != nullptr && + NormalizeHandleKey(pointer->data) == NormalizeHandleKey((void*)obj)) { + return handleCached; + } + } + } + + if (isRawHandleRoundTrip) { + return handleCached; + } + + removeCachedHandleObject(env, (void*)obj); } if (napi_value existing = getNormalizedObjectRef(env, obj); existing != nullptr) { @@ -640,6 +786,8 @@ napi_value findConstructorForObject(napi_env env, ObjCBridgeState* bridgeState, // #endif if (takeObjectRef(object) != nullptr) { + removeRecentObjectWrapper(env, object); + removeCachedHandleObject(env, (void*)object); [object release]; } } @@ -649,6 +797,8 @@ napi_value findConstructorForObject(napi_env env, ObjCBridgeState* bridgeState, return false; } + removeRecentObjectWrapper(env, object); + removeCachedHandleObject(env, (void*)object); [object release]; return true; } @@ -656,6 +806,8 @@ napi_value findConstructorForObject(napi_env env, ObjCBridgeState* bridgeState, void ObjCBridgeState::detachObject(id object) noexcept { takeObjectRef(object); removeRoundTripObject(object); + removeRecentObjectWrapper(env, object); + removeCachedHandleObject(env, (void*)object); if (object == nil) { return; diff --git a/NativeScript/ffi/ObjectRef.h b/NativeScript/ffi/napi/ObjectRef.h similarity index 100% rename from NativeScript/ffi/ObjectRef.h rename to NativeScript/ffi/napi/ObjectRef.h diff --git a/NativeScript/ffi/ObjectRef.mm b/NativeScript/ffi/napi/ObjectRef.mm similarity index 100% rename from NativeScript/ffi/ObjectRef.mm rename to NativeScript/ffi/napi/ObjectRef.mm diff --git a/NativeScript/ffi/Protocol.h b/NativeScript/ffi/napi/Protocol.h similarity index 100% rename from NativeScript/ffi/Protocol.h rename to NativeScript/ffi/napi/Protocol.h diff --git a/NativeScript/ffi/Protocol.mm b/NativeScript/ffi/napi/Protocol.mm similarity index 97% rename from NativeScript/ffi/Protocol.mm rename to NativeScript/ffi/napi/Protocol.mm index 1bbe911d..7cb65571 100644 --- a/NativeScript/ffi/Protocol.mm +++ b/NativeScript/ffi/napi/Protocol.mm @@ -64,7 +64,7 @@ // protocolOffsets[name] = originalOffset; auto objcProtocol = resolveRuntimeProtocol(name); if (objcProtocol != nil) { - mdProtocolsByPointer[objcProtocol] = originalOffset; + registerProtocolMetadata(objcProtocol, originalOffset); } while (next) { @@ -163,6 +163,7 @@ nameOffset &= ~mdSectionOffsetNext; name = bridgeState->metadata->resolveString(nameOffset); + bridgeState->registerRuntimeProtocol(this, resolveRuntimeProtocol(name.c_str())); napi_value constructor; napi_define_class(env, name.c_str(), NAPI_AUTO_LENGTH, ObjCProtocol::jsConstructor, nullptr, 0, diff --git a/NativeScript/ffi/SignatureDispatch.h b/NativeScript/ffi/napi/SignatureDispatch.h similarity index 78% rename from NativeScript/ffi/SignatureDispatch.h rename to NativeScript/ffi/napi/SignatureDispatch.h index 14367c8b..c42a8237 100644 --- a/NativeScript/ffi/SignatureDispatch.h +++ b/NativeScript/ffi/napi/SignatureDispatch.h @@ -1,13 +1,11 @@ -#ifndef NS_SIGNATURE_DISPATCH_H -#define NS_SIGNATURE_DISPATCH_H +#ifndef NS_FFI_NAPI_SIGNATURE_DISPATCH_H +#define NS_FFI_NAPI_SIGNATURE_DISPATCH_H #include -#include #include #include #include -#include #include "Cif.h" #include "js_native_api.h" @@ -17,11 +15,14 @@ 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); using ObjCNapiInvoker = bool (*)(napi_env env, Cif* cif, void* fnptr, id self, SEL selector, const napi_value* argv, void* rvalue); @@ -38,6 +39,11 @@ struct CFunctionDispatchEntry { CFunctionPreparedInvoker invoker; }; +struct BlockDispatchEntry { + uint64_t dispatchId; + BlockPreparedInvoker invoker; +}; + struct ObjCNapiDispatchEntry { uint64_t dispatchId; ObjCNapiInvoker invoker; @@ -73,6 +79,10 @@ inline uint64_t composeSignatureDispatchId(uint64_t signatureHash, } // namespace nativescript +#ifndef NS_GSD_BACKEND_NAPI +#define NS_GSD_BACKEND_NAPI 1 +#endif + #ifndef NS_HAS_GENERATED_SIGNATURE_DISPATCH #define NS_HAS_GENERATED_SIGNATURE_DISPATCH 0 #endif @@ -81,6 +91,26 @@ inline uint64_t composeSignatureDispatchId(uint64_t signatureHash, #define NS_HAS_GENERATED_SIGNATURE_NAPI_DISPATCH 0 #endif +#ifndef NS_GSD_BACKEND_V8 +#define NS_GSD_BACKEND_V8 0 +#endif + +#ifndef NS_GSD_BACKEND_JSC +#define NS_GSD_BACKEND_JSC 0 +#endif + +#ifndef NS_GSD_BACKEND_QUICKJS +#define NS_GSD_BACKEND_QUICKJS 0 +#endif + +#ifndef NS_GSD_BACKEND_HERMES +#define NS_GSD_BACKEND_HERMES 0 +#endif + +#ifndef NS_GSD_BACKEND_ENGINE_DIRECT +#define NS_GSD_BACKEND_ENGINE_DIRECT 0 +#endif + #if defined(__has_include) #if __has_include("GeneratedSignatureDispatch.inc") #include "GeneratedSignatureDispatch.inc" @@ -93,6 +123,8 @@ inline constexpr ObjCDispatchEntry kGeneratedObjCDispatchEntries[] = { {0, nullptr}}; inline constexpr CFunctionDispatchEntry kGeneratedCFunctionDispatchEntries[] = { {0, nullptr}}; +inline constexpr BlockDispatchEntry kGeneratedBlockDispatchEntries[] = { + {0, nullptr}}; } // namespace nativescript #endif @@ -156,10 +188,19 @@ inline CFunctionPreparedInvoker lookupCFunctionPreparedInvoker( if (!isGeneratedDispatchEnabled()) { return nullptr; } - return lookupDispatchInvoker( + return lookupDispatchInvoker( kGeneratedCFunctionDispatchEntries, dispatchId); } +inline BlockPreparedInvoker lookupBlockPreparedInvoker(uint64_t dispatchId) { + if (!isGeneratedDispatchEnabled()) { + return nullptr; + } + return lookupDispatchInvoker( + kGeneratedBlockDispatchEntries, dispatchId); +} + inline ObjCNapiInvoker lookupObjCNapiInvoker(uint64_t dispatchId) { if (!isGeneratedDispatchEnabled()) { return nullptr; @@ -172,10 +213,11 @@ inline CFunctionNapiInvoker lookupCFunctionNapiInvoker(uint64_t dispatchId) { if (!isGeneratedDispatchEnabled()) { return nullptr; } - return lookupDispatchInvoker( + return lookupDispatchInvoker( kGeneratedCFunctionNapiDispatchEntries, dispatchId); } } // namespace nativescript -#endif // NS_SIGNATURE_DISPATCH_H +#endif // NS_FFI_NAPI_SIGNATURE_DISPATCH_H diff --git a/NativeScript/ffi/Struct.h b/NativeScript/ffi/napi/Struct.h similarity index 100% rename from NativeScript/ffi/Struct.h rename to NativeScript/ffi/napi/Struct.h diff --git a/NativeScript/ffi/Struct.mm b/NativeScript/ffi/napi/Struct.mm similarity index 100% rename from NativeScript/ffi/Struct.mm rename to NativeScript/ffi/napi/Struct.mm diff --git a/NativeScript/ffi/TypeConv.h b/NativeScript/ffi/napi/TypeConv.h similarity index 99% rename from NativeScript/ffi/TypeConv.h rename to NativeScript/ffi/napi/TypeConv.h index 4e6dfcee..e3d024f2 100644 --- a/NativeScript/ffi/TypeConv.h +++ b/NativeScript/ffi/napi/TypeConv.h @@ -12,6 +12,8 @@ using namespace metagen; namespace nativescript { +class Cif; + typedef enum ConvertToJSFlags : uint32_t { kReturnOwned = 1 << 0, kBlockParam = 1 << 1, diff --git a/NativeScript/ffi/TypeConv.mm b/NativeScript/ffi/napi/TypeConv.mm similarity index 97% rename from NativeScript/ffi/TypeConv.mm rename to NativeScript/ffi/napi/TypeConv.mm index f82c8bb3..6a94b32d 100644 --- a/NativeScript/ffi/TypeConv.mm +++ b/NativeScript/ffi/napi/TypeConv.mm @@ -8,7 +8,7 @@ #include "MetadataReader.h" #include "ObjCBridge.h" #include "ffi.h" -#include "ffi/Struct.h" +#include "Struct.h" #include "js_native_api.h" #include "js_native_api_types.h" #include "node_api_util.h" @@ -38,6 +38,41 @@ + (void)transferOwnership:(napi_env)env of:(napi_value)value toNative:(id)object namespace { +static napi_value findRegisteredClassConstructor(napi_env env, Class cls) { + if (env == nullptr || cls == nil) { + return nullptr; + } + + const char* runtimeName = class_getName(cls); + if (runtimeName == nullptr || runtimeName[0] == '\0') { + return nullptr; + } + + napi_value global = nullptr; + napi_value classRegistry = nullptr; + bool hasClassRegistry = false; + if (napi_get_global(env, &global) != napi_ok || global == nullptr || + napi_has_named_property(env, global, "__nsConstructorsByObjCClassName", + &hasClassRegistry) != napi_ok || + !hasClassRegistry || + napi_get_named_property(env, global, "__nsConstructorsByObjCClassName", + &classRegistry) != napi_ok || + classRegistry == nullptr) { + return nullptr; + } + + bool hasConstructor = false; + napi_value constructor = nullptr; + if (napi_has_named_property(env, classRegistry, runtimeName, &hasConstructor) == napi_ok && + hasConstructor && + napi_get_named_property(env, classRegistry, runtimeName, &constructor) == napi_ok && + constructor != nullptr) { + return constructor; + } + + return nullptr; +} + static size_t getBufferElementSize(napi_typedarray_type type) { switch (type) { case napi_int8_array: @@ -215,13 +250,22 @@ static id resolveCachedHandleObject(napi_env env, void* handle) { if (napi_has_named_property(env, cachedValue, "__ns_native_ptr", &hasNativePointer) == napi_ok && hasNativePointer) { napi_value nativePointerValue = nullptr; - void* nativePointer = nullptr; if (napi_get_named_property(env, cachedValue, "__ns_native_ptr", &nativePointerValue) == - napi_ok && - napi_get_value_external(env, nativePointerValue, &nativePointer) == napi_ok && - nativePointer != nullptr) { - bridgeState->cacheRoundTripObject(env, static_cast(nativePointer), cachedValue); - return static_cast(nativePointer); + napi_ok) { + if (nativescript::Pointer::isInstance(env, nativePointerValue)) { + nativescript::Pointer* pointer = nativescript::Pointer::unwrap(env, nativePointerValue); + if (pointer != nullptr && pointer->data != nullptr) { + bridgeState->cacheRoundTripObject(env, static_cast(pointer->data), cachedValue); + return static_cast(pointer->data); + } + } else { + void* nativePointer = nullptr; + if (napi_get_value_external(env, nativePointerValue, &nativePointer) == napi_ok && + nativePointer != nullptr) { + bridgeState->cacheRoundTripObject(env, static_cast(nativePointer), cachedValue); + return static_cast(nativePointer); + } + } } } @@ -1223,8 +1267,9 @@ napi_value toJS(napi_env env, void* value, uint32_t flags) override { MDSectionOffset metadataOffset = findProtocolMetadataOffset(bridgeState->metadata, runtimeName); if (metadataOffset != MD_SECTION_OFFSET_NULL) { - bridgeState->mdProtocolsByPointer[runtimeProto] = metadataOffset; + bridgeState->registerProtocolMetadata(runtimeProto, metadataOffset); auto proto = bridgeState->getProtocol(env, metadataOffset); + bridgeState->registerRuntimeProtocol(proto, runtimeProto); if (proto != nullptr) { ::free(protocols); return get_ref_value(env, proto->constructor); @@ -1272,6 +1317,21 @@ void toNative(napi_env env, napi_value value, void* result, bool* shouldFree, void* wrapped = nullptr; napi_status unwrapStatus = napi_unwrap(env, input, &wrapped); if (unwrapStatus != napi_ok) { + bool hasNativePointer = false; + if (napi_has_named_property(env, input, "__ns_native_ptr", &hasNativePointer) == + napi_ok && + hasNativePointer) { + napi_value nativePointerValue = nullptr; + if (napi_get_named_property(env, input, "__ns_native_ptr", &nativePointerValue) == + napi_ok && + Pointer::isInstance(env, nativePointerValue)) { + Pointer* pointer = Pointer::unwrap(env, nativePointerValue); + if (pointer != nullptr && pointer->data != nullptr) { + *out = pointer->data; + return true; + } + } + } return false; } @@ -2119,6 +2179,13 @@ napi_value toJS(napi_env env, void* value, uint32_t flags) override { auto bridgeState = ObjCBridgeState::InstanceData(env); if (bridgeState != nullptr) { + if (object_isClass(obj)) { + if (napi_value constructor = findRegisteredClassConstructor(env, (Class)obj); + constructor != nullptr) { + return constructor; + } + } + auto normalizePtr = [](void* ptr) -> uintptr_t { return normalizeRuntimePointer(reinterpret_cast(ptr)); }; @@ -2612,6 +2679,7 @@ void toNative(napi_env env, napi_value value, void* result, bool* shouldFree, } *res = (id)wrapped; + cacheRoundTrip(*res); return; break; @@ -2752,19 +2820,24 @@ void toNative(napi_env env, napi_value value, void* result, bool* shouldFree, kind = mdTypeClass; } - napi_value toJS(napi_env env, void* value, uint32_t flags) override { - Class cls = *((Class*)value); + napi_value toJS(napi_env env, void* value, uint32_t flags) override { + Class cls = *((Class*)value); - if (cls == nullptr) { - napi_value null; - napi_get_null(env, &null); - return null; - } + if (cls == nullptr) { + napi_value null; + napi_get_null(env, &null); + return null; + } - auto bridgeState = ObjCBridgeState::InstanceData(env); - return bridgeState != nullptr ? bridgeState->getObject(env, (id)cls, kUnownedObject, 0, nullptr) - : nullptr; - } + if (napi_value constructor = findRegisteredClassConstructor(env, cls); + constructor != nullptr) { + return constructor; + } + + auto bridgeState = ObjCBridgeState::InstanceData(env); + return bridgeState != nullptr ? bridgeState->getObject(env, (id)cls, kUnownedObject, 0, nullptr) + : nullptr; + } void toNative(napi_env env, napi_value value, void* result, bool* shouldFree, bool* shouldFreeAny) override { @@ -3916,6 +3989,7 @@ bool tryFastConvertObjCObjectValue(napi_env env, napi_value value, napi_valuetyp } *out = (id)wrapped; + cacheRoundTrip(*out); return true; } diff --git a/NativeScript/ffi/Util.h b/NativeScript/ffi/napi/Util.h similarity index 100% rename from NativeScript/ffi/Util.h rename to NativeScript/ffi/napi/Util.h diff --git a/NativeScript/ffi/Util.mm b/NativeScript/ffi/napi/Util.mm similarity index 100% rename from NativeScript/ffi/Util.mm rename to NativeScript/ffi/napi/Util.mm diff --git a/NativeScript/ffi/Variable.h b/NativeScript/ffi/napi/Variable.h similarity index 100% rename from NativeScript/ffi/Variable.h rename to NativeScript/ffi/napi/Variable.h diff --git a/NativeScript/ffi/Variable.mm b/NativeScript/ffi/napi/Variable.mm similarity index 100% rename from NativeScript/ffi/Variable.mm rename to NativeScript/ffi/napi/Variable.mm diff --git a/NativeScript/ffi/node_api_util.h b/NativeScript/ffi/napi/node_api_util.h similarity index 100% rename from NativeScript/ffi/node_api_util.h rename to NativeScript/ffi/napi/node_api_util.h diff --git a/NativeScript/ffi/quickjs/NativeApiQuickJS.h b/NativeScript/ffi/quickjs/NativeApiQuickJS.h new file mode 100644 index 00000000..73c31984 --- /dev/null +++ b/NativeScript/ffi/quickjs/NativeApiQuickJS.h @@ -0,0 +1,20 @@ +#ifndef NATIVESCRIPT_FFI_QUICKJS_NATIVE_API_QUICKJS_H +#define NATIVESCRIPT_FFI_QUICKJS_NATIVE_API_QUICKJS_H + +#include "ffi/shared/direct/NativeApiDirect.h" +#include "quickjs.h" + +namespace nativescript { + +using NativeApiQuickJSConfig = NativeApiDirectConfig; + +void InstallNativeApiQuickJS(JSContext* context, + const NativeApiQuickJSConfig& config = + NativeApiQuickJSConfig{}); + +} // namespace nativescript + +extern "C" void NativeScriptInstallNativeApiQuickJS(JSContext* context, + const char* metadataPath); + +#endif // NATIVESCRIPT_FFI_QUICKJS_NATIVE_API_QUICKJS_H diff --git a/NativeScript/ffi/quickjs/NativeApiQuickJS.mm b/NativeScript/ffi/quickjs/NativeApiQuickJS.mm new file mode 100644 index 00000000..57988a26 --- /dev/null +++ b/NativeScript/ffi/quickjs/NativeApiQuickJS.mm @@ -0,0 +1,159 @@ +#include "NativeApiQuickJS.h" + +#ifdef TARGET_ENGINE_QUICKJS + +#include "NativeApiQuickJSRuntime.h" + +namespace nativescript { + +using NativeApiJsiConfig = NativeApiDirectConfig; +using NativeApiJsiScheduler = NativeApiDirectScheduler; + +namespace { + +using facebook::jsi::Array; +using facebook::jsi::ArrayBuffer; +using facebook::jsi::BigInt; +using facebook::jsi::Function; +using facebook::jsi::HostObject; +using facebook::jsi::MutableBuffer; +using facebook::jsi::Object; +using facebook::jsi::PropNameID; +using facebook::jsi::Runtime; +using facebook::jsi::String; +using facebook::jsi::StringBuffer; +using facebook::jsi::Value; +using metagen::MDMemberFlag; +using metagen::MDMetadataReader; +using metagen::MDSectionOffset; +using metagen::MDTypeKind; + +// clang-format off +#include "jsi/NativeApiJsiBridge.h" +// clang-format on + +#define NATIVESCRIPT_NATIVE_API_HAS_ENGINE_LAZY_GLOBALS 1 +#define NATIVESCRIPT_NATIVE_API_RETAIN_RUNTIME 1 + +static JSValue NativeApiQuickJSLazyGlobalGetter(JSContext* context, JSValueConst, int, + JSValueConst*, int, JSValueConst* data) { + JSValue global = JS_GetGlobalObject(context); + JSValue resolver = JS_GetPropertyStr(context, global, "__nativeScriptResolveNativeApiLazyGlobal"); + if (!JS_IsFunction(context, resolver)) { + JS_FreeValue(context, resolver); + JS_FreeValue(context, global); + return JS_UNDEFINED; + } + + JSValueConst args[] = {data[0], data[1]}; + JSValue result = JS_Call(context, resolver, global, 2, args); + JS_FreeValue(context, resolver); + if (JS_IsException(result)) { + JS_FreeValue(context, global); + return result; + } + + JSAtom atom = JS_ValueToAtom(context, data[0]); + if (atom != JS_ATOM_NULL) { + JS_DefinePropertyValue(context, global, atom, JS_DupValue(context, result), + JS_PROP_CONFIGURABLE); + JS_FreeAtom(context, atom); + } + JS_FreeValue(context, global); + return result; +} + +bool InstallNativeApiEngineLazyGlobal(Runtime& runtime, std::shared_ptr, + const std::string& name, const std::string& kind, + bool force) { + if (name.empty() || kind.empty()) { + return false; + } + + JSContext* context = runtime.context(); + JSValue global = JS_GetGlobalObject(context); + JSAtom atom = JS_NewAtomLen(context, name.data(), name.size()); + if (atom == JS_ATOM_NULL) { + JS_FreeValue(context, global); + return false; + } + + int hasProperty = JS_HasProperty(context, global, atom); + if (!force && hasProperty > 0) { + JS_FreeAtom(context, atom); + JS_FreeValue(context, global); + return false; + } + if (hasProperty < 0) { + JS_FreeAtom(context, atom); + JS_FreeValue(context, global); + return false; + } + + JSValue data[] = { + JS_NewStringLen(context, name.data(), name.size()), + JS_NewStringLen(context, kind.data(), kind.size()), + }; + if (JS_IsException(data[0]) || JS_IsException(data[1])) { + JS_FreeValue(context, data[0]); + JS_FreeValue(context, data[1]); + JS_FreeAtom(context, atom); + JS_FreeValue(context, global); + return false; + } + + JSValue getter = JS_NewCFunctionData(context, NativeApiQuickJSLazyGlobalGetter, 0, 0, 2, data); + JS_FreeValue(context, data[0]); + JS_FreeValue(context, data[1]); + if (JS_IsException(getter)) { + JS_FreeAtom(context, atom); + JS_FreeValue(context, global); + return false; + } + + int status = + JS_DefinePropertyGetSet(context, global, atom, getter, JS_UNDEFINED, JS_PROP_CONFIGURABLE); + JS_FreeAtom(context, atom); + JS_FreeValue(context, global); + return status >= 0; +} + +// clang-format off +#include "jsi/NativeApiJsiHostObjects.h" +// clang-format on + +std::shared_ptr retainNativeApiJsiRuntime(Runtime& runtime) { + return std::make_shared(runtime.state()); +} + +// clang-format off +#include "jsi/NativeApiJsiCallbacks.h" +#include "jsi/NativeApiJsiConversion.h" +#include "jsi/NativeApiJsiInvocation.h" +#include "jsi/NativeApiJsiClassBuilder.h" +#include "jsi/NativeApiJsiHostObject.h" +// clang-format on + +} // namespace + +#include "jsi/NativeApiJsiInstall.h" + +void InstallNativeApiQuickJS(JSContext* context, const NativeApiQuickJSConfig& config) { + if (context == nullptr) { + return; + } + auto state = facebook::jsi::quickjsdirect::stateForContext(context); + facebook::jsi::Runtime runtime(state); + facebook::jsi::quickjsdirect::ensureClasses(runtime); + InstallNativeApiJSI(runtime, config); +} + +} // namespace nativescript + +extern "C" void NativeScriptInstallNativeApiQuickJS(JSContext* context, const char* metadataPath) { + nativescript::NativeApiQuickJSConfig config; + config.metadataPath = metadataPath; + nativescript::InstallNativeApiQuickJS(context, config); +} + +#endif // TARGET_ENGINE_QUICKJS diff --git a/NativeScript/ffi/quickjs/NativeApiQuickJSHostObjects.mm b/NativeScript/ffi/quickjs/NativeApiQuickJSHostObjects.mm new file mode 100644 index 00000000..1cd706a3 --- /dev/null +++ b/NativeScript/ffi/quickjs/NativeApiQuickJSHostObjects.mm @@ -0,0 +1,239 @@ +#include "NativeApiQuickJSRuntime.h" + +#ifdef TARGET_ENGINE_QUICKJS + +namespace facebook { +namespace jsi { + +namespace quickjsdirect { + +JSClassID gHostClassId = 0; +JSClassID gFunctionClassId = 0; + +namespace { +std::mutex& runtimeStatesMutex() { + static auto* mutex = new std::mutex(); + return *mutex; +} + +std::unordered_map>& runtimeStates() { + static auto* states = new std::unordered_map>(); + return *states; +} +} // namespace + +std::shared_ptr stateForContext(JSContext* context) { + std::lock_guard lock(runtimeStatesMutex()); + auto& states = runtimeStates(); + auto it = states.find(context); + if (it != states.end()) { + return it->second; + } + auto state = std::make_shared(context); + states[context] = state; + return state; +} + +static JSValue nativeHostGet(JSContext* ctx, JSValueConst obj, JSAtom atom, JSValueConst receiver) { + (void)receiver; + Runtime runtime(stateForContext(ctx)); + auto* holder = static_cast(JS_GetOpaque(obj, gHostClassId)); + if (holder == nullptr || holder->hostObject == nullptr) { + return JS_UNDEFINED; + } + try { + Value result = holder->hostObject->get(runtime, PropNameID(atomToUtf8(ctx, atom))); + return result.local(runtime); + } catch (const std::exception& error) { + return throwError(ctx, error); + } +} + +static int nativeHostSet(JSContext* ctx, JSValueConst obj, JSAtom atom, JSValueConst value, + JSValueConst, int) { + Runtime runtime(stateForContext(ctx)); + auto* holder = static_cast(JS_GetOpaque(obj, gHostClassId)); + if (holder == nullptr || holder->hostObject == nullptr) { + return 0; + } + try { + holder->hostObject->set(runtime, PropNameID(atomToUtf8(ctx, atom)), Value(runtime, value)); + return 1; + } catch (const std::exception& error) { + throwError(ctx, error); + return -1; + } +} + +static int nativeHostHas(JSContext* ctx, JSValueConst obj, JSAtom atom) { + Runtime runtime(stateForContext(ctx)); + auto* holder = static_cast(JS_GetOpaque(obj, gHostClassId)); + if (holder == nullptr || holder->hostObject == nullptr) { + return 0; + } + try { + auto names = holder->hostObject->getPropertyNames(runtime); + std::string requested = atomToUtf8(ctx, atom); + for (const auto& name : names) { + if (name.utf8(runtime) == requested) { + return 1; + } + } + } catch (const std::exception&) { + } + return 0; +} + +static int nativeHostOwnNames(JSContext* ctx, JSPropertyEnum** ptab, uint32_t* plen, + JSValueConst obj) { + Runtime runtime(stateForContext(ctx)); + auto* holder = static_cast(JS_GetOpaque(obj, gHostClassId)); + if (holder == nullptr || holder->hostObject == nullptr) { + *ptab = nullptr; + *plen = 0; + return 0; + } + auto names = holder->hostObject->getPropertyNames(runtime); + *plen = static_cast(names.size()); + *ptab = static_cast(js_mallocz(ctx, sizeof(JSPropertyEnum) * names.size())); + for (uint32_t i = 0; i < *plen; i++) { + (*ptab)[i].is_enumerable = true; + (*ptab)[i].atom = JS_NewAtom(ctx, names[i].utf8(runtime).c_str()); + } + return 0; +} + +static void nativeHostFinalize(JSRuntime*, JSValue value) { + auto* holder = static_cast(JS_GetOpaque(value, gHostClassId)); + delete holder; +} + +static JSValue invokeFunctionHolder(JSContext* ctx, FunctionHolder* holder, JSValueConst thisValue, + int argc, JSValueConst* argv) { + Runtime runtime(stateForContext(ctx)); + if (holder == nullptr || !holder->callback) { + return JS_UNDEFINED; + } + std::vector args; + args.reserve(argc); + for (int i = 0; i < argc; i++) { + args.emplace_back(runtime, argv[i]); + } + try { + Value self(runtime, thisValue); + Value result = + holder->callback(runtime, self, args.empty() ? nullptr : args.data(), args.size()); + return result.local(runtime); + } catch (const std::exception& error) { + return throwError(ctx, error); + } +} + +static JSValue nativeFunctionCall(JSContext* ctx, JSValue function, JSValue thisValue, int argc, + JSValue* argv, int) { + auto* holder = static_cast(JS_GetOpaque(function, gFunctionClassId)); + return invokeFunctionHolder(ctx, holder, thisValue, argc, argv); +} + +static JSValue nativeFunctionCallData(JSContext* ctx, JSValue thisValue, int argc, JSValue* argv, + int, JSValue* data) { + auto* holder = static_cast(JS_GetOpaque(data[0], gFunctionClassId)); + return invokeFunctionHolder(ctx, holder, thisValue, argc, argv); +} + +static void nativeFunctionFinalize(JSRuntime*, JSValue value) { + auto* holder = static_cast(JS_GetOpaque(value, gFunctionClassId)); + delete holder; +} + +static JSClassExoticMethods hostExoticMethods = { + .get_own_property = nullptr, + .get_own_property_names = nativeHostOwnNames, + .delete_property = nullptr, + .define_own_property = nullptr, + .has_property = nativeHostHas, + .get_property = nativeHostGet, + .set_property = nativeHostSet, +}; + +void ensureClasses(Runtime& runtime) { + auto state = runtime.state(); + JSRuntime* rt = JS_GetRuntime(runtime.context()); + if (gHostClassId == 0) { + JS_NewClassID(rt, &gHostClassId); + } + if (!state->hostClassRegistered) { + JSClassDef def = {}; + def.class_name = "NativeScriptDirectHostObject"; + def.exotic = &hostExoticMethods; + def.finalizer = nativeHostFinalize; + JS_NewClass(rt, gHostClassId, &def); + JS_SetClassProto(runtime.context(), gHostClassId, JS_NewObject(runtime.context())); + state->hostClassRegistered = true; + } + if (gFunctionClassId == 0) { + JS_NewClassID(rt, &gFunctionClassId); + } + if (!state->functionClassRegistered) { + JSClassDef def = {}; + def.class_name = "NativeScriptDirectFunction"; + def.call = nativeFunctionCall; + def.finalizer = nativeFunctionFinalize; + JS_NewClass(rt, gFunctionClassId, &def); + JS_SetClassProto(runtime.context(), gFunctionClassId, JS_NewObject(runtime.context())); + state->functionClassRegistered = true; + } +} + +} // namespace quickjsdirect + +quickjsdirect::HostObjectHolder* Object::hostObjectHolder(Runtime& runtime) const { + quickjsdirect::ensureClasses(runtime); + JSValue object = local(runtime); + auto* holder = static_cast( + JS_GetOpaque(object, quickjsdirect::gHostClassId)); + JS_FreeValue(runtime.context(), object); + return holder; +} + +Object Object::createFromHostObjectWithToken(Runtime& runtime, std::shared_ptr host, + const void* typeToken) { + quickjsdirect::ensureClasses(runtime); + auto* holder = new quickjsdirect::HostObjectHolder(runtime.state(), std::move(host), typeToken); + JSValue object = JS_NewObjectClass(runtime.context(), quickjsdirect::gHostClassId); + JS_SetOpaque(object, holder); + Object result = Object::fromValueStorage(Value(runtime, object).storage_); + JS_FreeValue(runtime.context(), object); + return result; +} + +Function Function::createFromHostFunction(Runtime& runtime, const PropNameID& name, + unsigned int parameterCount, HostFunctionType callback) { + quickjsdirect::ensureClasses(runtime); + auto* holder = new quickjsdirect::FunctionHolder(runtime.state(), std::move(callback)); + JSValue data = JS_NewObjectClass(runtime.context(), quickjsdirect::gFunctionClassId); + if (JS_IsException(data)) { + delete holder; + throw JSError(runtime, "QuickJS host function data allocation failed."); + } + JS_SetOpaque(data, holder); + + JSValue function = JS_NewCFunctionData(runtime.context(), quickjsdirect::nativeFunctionCallData, + static_cast(parameterCount), 0, 1, &data); + JS_FreeValue(runtime.context(), data); + if (JS_IsException(function)) { + throw JSError(runtime, "QuickJS host function allocation failed."); + } + + std::string functionName = name.utf8(runtime); + JSValue nameValue = JS_NewStringLen(runtime.context(), functionName.data(), functionName.size()); + JS_DefinePropertyValueStr(runtime.context(), function, "name", nameValue, JS_PROP_CONFIGURABLE); + Function result = Function(Object::fromValueStorage(Value(runtime, function).storage_)); + JS_FreeValue(runtime.context(), function); + return result; +} + +} // namespace jsi +} // namespace facebook + +#endif // TARGET_ENGINE_QUICKJS diff --git a/NativeScript/ffi/quickjs/NativeApiQuickJSRuntime.h b/NativeScript/ffi/quickjs/NativeApiQuickJSRuntime.h new file mode 100644 index 00000000..438a7816 --- /dev/null +++ b/NativeScript/ffi/quickjs/NativeApiQuickJSRuntime.h @@ -0,0 +1,745 @@ +#ifndef NATIVESCRIPT_FFI_QUICKJS_NATIVE_API_QUICKJS_RUNTIME_H +#define NATIVESCRIPT_FFI_QUICKJS_NATIVE_API_QUICKJS_RUNTIME_H + +#ifdef TARGET_ENGINE_QUICKJS + +#import +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Metadata.h" +#include "MetadataReader.h" +#include "ffi.h" +#include "quickjs.h" + +@protocol NativeApiJsiClassBuilderProtocol +@end + +#ifdef EMBED_METADATA_SIZE +extern const unsigned char embedded_metadata[EMBED_METADATA_SIZE]; +#endif + +namespace facebook { +namespace jsi { + +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 void set(Runtime& runtime, const PropNameID& name, const Value& value); + virtual std::vector getPropertyNames(Runtime& runtime); +}; + +using HostFunctionType = std::function; + +namespace quickjsdirect { + +template +const void* hostObjectTypeToken() { + static int token = 0; + return &token; +} + +struct RuntimeState { + explicit RuntimeState(JSContext* context) : context(context) {} + JSContext* context = nullptr; + bool hostClassRegistered = false; + bool functionClassRegistered = false; +}; + +extern JSClassID gHostClassId; +extern JSClassID gFunctionClassId; + +std::shared_ptr stateForContext(JSContext* context); + +struct ValueStorage { + enum class Kind { + Undefined, + Null, + Bool, + Number, + QuickJS, + }; + + explicit ValueStorage(Kind kind) : kind(kind) {} + ~ValueStorage() { + if (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 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 quickjsdirect + +class Runtime { + public: + explicit Runtime(JSContext* context) : state_(quickjsdirect::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_; } + 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() + : storage_(std::make_shared( + quickjsdirect::ValueStorage::Kind::Undefined)) {} + Value(bool value) + : storage_(std::make_shared( + quickjsdirect::ValueStorage::Kind::Bool)) { + storage_->boolValue = value; + } + Value(double value) + : storage_(std::make_shared( + quickjsdirect::ValueStorage::Kind::Number)) { + storage_->numberValue = value; + } + Value(int value) : Value(static_cast(value)) {} + Value(uint32_t value) : Value(static_cast(value)) {} + + Value(Runtime& runtime, const Value& value) : storage_(value.storage_) {} + Value(Runtime& runtime, Value&& value) : storage_(std::move(value.storage_)) {} + Value(Runtime& runtime, const String& value) : storage_(value.storage_) {} + 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) + : storage_(std::make_shared( + quickjsdirect::ValueStorage::Kind::QuickJS)) { + storage_->context = runtime.context(); + storage_->value = JS_DupValue(runtime.context(), value); + } + + static Value undefined() { return Value(); } + static Value null() { + Value value; + value.storage_ = + std::make_shared(quickjsdirect::ValueStorage::Kind::Null); + return value; + } + bool isUndefined() const { + return storage_->kind == quickjsdirect::ValueStorage::Kind::Undefined || + (storage_->kind == quickjsdirect::ValueStorage::Kind::QuickJS && + JS_IsUndefined(storage_->value)); + } + bool isNull() const { + return storage_->kind == quickjsdirect::ValueStorage::Kind::Null || + (storage_->kind == quickjsdirect::ValueStorage::Kind::QuickJS && + JS_IsNull(storage_->value)); + } + bool isBool() const { + return storage_->kind == quickjsdirect::ValueStorage::Kind::Bool || + (storage_->kind == quickjsdirect::ValueStorage::Kind::QuickJS && + JS_IsBool(storage_->value)); + } + bool getBool() const { + if (storage_->kind == quickjsdirect::ValueStorage::Kind::Bool) { + return storage_->boolValue; + } + if (storage_->kind == quickjsdirect::ValueStorage::Kind::QuickJS) { + return JS_ToBool(storage_->context, storage_->value) != 0; + } + return false; + } + bool isNumber() const { + return storage_->kind == quickjsdirect::ValueStorage::Kind::Number || + (storage_->kind == quickjsdirect::ValueStorage::Kind::QuickJS && + JS_IsNumber(storage_->value)); + } + double getNumber() const { + if (storage_->kind == quickjsdirect::ValueStorage::Kind::Number) { + return storage_->numberValue; + } + if (storage_->kind == quickjsdirect::ValueStorage::Kind::QuickJS) { + double value = 0; + JS_ToFloat64(storage_->context, &value, storage_->value); + return value; + } + return 0; + } + bool isObject() const { + return storage_->kind == quickjsdirect::ValueStorage::Kind::QuickJS && + JS_IsObject(storage_->value); + } + bool isString() const { + return storage_->kind == quickjsdirect::ValueStorage::Kind::QuickJS && + JS_IsString(storage_->value); + } + bool isBigInt() const { + return storage_->kind == quickjsdirect::ValueStorage::Kind::QuickJS && + JS_IsBigInt(storage_->context, storage_->value); + } + bool isSymbol() const { + return storage_->kind == quickjsdirect::ValueStorage::Kind::QuickJS && + JS_IsSymbol(storage_->value); + } + + Object asObject(Runtime& runtime) const; + String asString(Runtime& runtime) const; + BigInt getBigInt(Runtime& runtime) const; + + JSValue local(Runtime& runtime) const { + switch (storage_->kind) { + case quickjsdirect::ValueStorage::Kind::Undefined: + return JS_UNDEFINED; + case quickjsdirect::ValueStorage::Kind::Null: + return JS_NULL; + case quickjsdirect::ValueStorage::Kind::Bool: + return JS_NewBool(runtime.context(), storage_->boolValue); + case quickjsdirect::ValueStorage::Kind::Number: + return JS_NewFloat64(runtime.context(), storage_->numberValue); + case quickjsdirect::ValueStorage::Kind::QuickJS: + return JS_DupValue(runtime.context(), storage_->value); + } + } + + private: + friend class Runtime; + friend class Object; + friend class String; + friend class BigInt; + friend class ArrayBuffer; + friend class Function; + friend class Array; + std::shared_ptr storage_; +}; + +class Object { + public: + Object() = default; + explicit Object(Runtime& runtime) + : storage_(std::make_shared( + quickjsdirect::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), + quickjsdirect::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(runtime.context(), 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 == quickjsdirect::hostObjectTypeToken(); + } + template + std::shared_ptr getHostObject(Runtime& runtime) const { + auto holder = hostObjectHolder(runtime); + if (holder == nullptr || holder->typeToken != quickjsdirect::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 { + Value value; + value.storage_ = storage_; + return value; + } + + 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); + quickjsdirect::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, "QuickJS function call failed."); + } + 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, "QuickJS function call failed."); + } + 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( + quickjsdirect::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( + quickjsdirect::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 { + Value value; + value.storage_ = storage_; + return value; + } + + private: + friend class Value; + std::shared_ptr storage_; +}; + +class ArrayBuffer : public Object { + public: + ArrayBuffer(Runtime& runtime, std::shared_ptr buffer) + : Object(std::make_shared( + quickjsdirect::ValueStorage::Kind::QuickJS)) { + auto* holder = new quickjsdirect::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 jsi +} // namespace facebook + +#endif // TARGET_ENGINE_QUICKJS + +#endif // NATIVESCRIPT_FFI_QUICKJS_NATIVE_API_QUICKJS_RUNTIME_H diff --git a/NativeScript/ffi/quickjs/NativeApiQuickJSRuntime.mm b/NativeScript/ffi/quickjs/NativeApiQuickJSRuntime.mm new file mode 100644 index 00000000..6a64b8e5 --- /dev/null +++ b/NativeScript/ffi/quickjs/NativeApiQuickJSRuntime.mm @@ -0,0 +1,40 @@ +#include "NativeApiQuickJSRuntime.h" + +#ifdef TARGET_ENGINE_QUICKJS + +namespace facebook { +namespace jsi { + +String BigInt::toString(Runtime& runtime, int) const { + JSValue value = local(runtime); + JSValue stringValue = JS_ToString(runtime.context(), value); + JS_FreeValue(runtime.context(), value); + String result(runtime, stringValue); + JS_FreeValue(runtime.context(), stringValue); + return result; +} + +Object Runtime::global() { + JSValue global = JS_GetGlobalObject(context()); + Object result = Object::fromValueStorage(Value(*this, global).storage_); + JS_FreeValue(context(), global); + return result; +} + +Value Runtime::evaluateJavaScript(std::shared_ptr buffer, + const std::string& sourceURL) { + JSValue result = + JS_Eval(context(), buffer != nullptr ? buffer->data() : "", + buffer != nullptr ? buffer->size() : 0, sourceURL.c_str(), JS_EVAL_TYPE_GLOBAL); + if (JS_IsException(result)) { + throw JSError(*this, "QuickJS script evaluation failed."); + } + Value value(*this, result); + JS_FreeValue(context(), result); + return value; +} + +} // namespace jsi +} // namespace facebook + +#endif // TARGET_ENGINE_QUICKJS diff --git a/NativeScript/ffi/quickjs/NativeApiQuickJSValue.mm b/NativeScript/ffi/quickjs/NativeApiQuickJSValue.mm new file mode 100644 index 00000000..94ae05e0 --- /dev/null +++ b/NativeScript/ffi/quickjs/NativeApiQuickJSValue.mm @@ -0,0 +1,88 @@ +#include "NativeApiQuickJSRuntime.h" + +#ifdef TARGET_ENGINE_QUICKJS + +namespace facebook { +namespace jsi { + +Value HostObject::get(Runtime&, const PropNameID&) { return Value::undefined(); } +void HostObject::set(Runtime&, const PropNameID&, const Value&) {} +std::vector HostObject::getPropertyNames(Runtime&) { return {}; } +String::String(Runtime& runtime, JSValue value) + : storage_(std::make_shared( + quickjsdirect::ValueStorage::Kind::QuickJS)) { + storage_->context = runtime.context(); + storage_->value = JS_DupValue(runtime.context(), value); +} +std::string String::utf8(Runtime& runtime) const { + JSValue value = local(runtime); + std::string result = quickjsdirect::valueToUtf8(runtime.context(), value); + JS_FreeValue(runtime.context(), value); + return result; +} +JSValue String::local(Runtime& runtime) const { + return JS_DupValue(runtime.context(), storage_->value); +} +String::operator Value() const { + Value value; + value.storage_ = storage_; + return value; +} +Value::Value(Runtime&, const Object& object) : storage_(object.storage_) {} +Value::Value(Runtime&, const Function& function) : storage_(function.storage_) {} +Value::Value(Runtime&, const Array& array) : storage_(array.storage_) {} +Value::Value(Runtime&, const ArrayBuffer& arrayBuffer) : storage_(arrayBuffer.storage_) {} +Value::Value(Runtime&, const BigInt& bigint) : storage_(bigint.storage_) {} +Object Value::asObject(Runtime&) const { return Object::fromValueStorage(storage_); } +String Value::asString(Runtime& runtime) const { + JSValue value = local(runtime); + String result(runtime, value); + JS_FreeValue(runtime.context(), value); + return result; +} +BigInt Value::getBigInt(Runtime& runtime) const { + JSValue value = local(runtime); + BigInt result(runtime, value); + JS_FreeValue(runtime.context(), value); + return result; +} +Function Object::getPropertyAsFunction(Runtime& runtime, const char* name) const { + return getProperty(runtime, name).asObject(runtime).asFunction(runtime); +} +Function Object::asFunction(Runtime&) const { return Function(*this); } +Array Object::getArray(Runtime&) const { return Array(*this); } +ArrayBuffer Object::getArrayBuffer(Runtime&) const { return ArrayBuffer(*this); } +Array Object::getPropertyNames(Runtime& runtime) const { + JSValue object = local(runtime); + JSPropertyEnum* properties = nullptr; + uint32_t count = 0; + int status = JS_GetOwnPropertyNames(runtime.context(), &properties, &count, object, + JS_GPN_STRING_MASK | JS_GPN_SYMBOL_MASK | JS_GPN_ENUM_ONLY); + JS_FreeValue(runtime.context(), object); + if (status < 0) { + throw JSError(runtime, "QuickJS property names failed."); + } + Array result(runtime, count); + for (uint32_t i = 0; i < count; i++) { + JSValue nameValue = JS_AtomToValue(runtime.context(), properties[i].atom); + result.setValueAtIndex(runtime, i, Value(runtime, nameValue)); + JS_FreeValue(runtime.context(), nameValue); + JS_FreeAtom(runtime.context(), properties[i].atom); + } + js_free(runtime.context(), properties); + return result; +} +void Object::setProperty(Runtime& runtime, const char* name, const Function& value) { + setProperty(runtime, name, Value(runtime, value)); +} +void Object::setProperty(Runtime& runtime, const char* name, const Array& value) { + setProperty(runtime, name, Value(runtime, value)); +} +void Object::setProperty(Runtime& runtime, const char* name, const ArrayBuffer& value) { + setProperty(runtime, name, Value(runtime, value)); +} + +} // namespace jsi +} // namespace facebook + +#endif // TARGET_ENGINE_QUICKJS diff --git a/NativeScript/ffi/Tasks.cpp b/NativeScript/ffi/shared/Tasks.cpp similarity index 100% rename from NativeScript/ffi/Tasks.cpp rename to NativeScript/ffi/shared/Tasks.cpp diff --git a/NativeScript/ffi/Tasks.h b/NativeScript/ffi/shared/Tasks.h similarity index 100% rename from NativeScript/ffi/Tasks.h rename to NativeScript/ffi/shared/Tasks.h diff --git a/NativeScript/ffi/shared/direct/EmbeddedMetadata.mm b/NativeScript/ffi/shared/direct/EmbeddedMetadata.mm new file mode 100644 index 00000000..6311acde --- /dev/null +++ b/NativeScript/ffi/shared/direct/EmbeddedMetadata.mm @@ -0,0 +1,12 @@ +#include + +#ifdef EMBED_METADATA_SIZE + +extern const unsigned char +#if TARGET_CPU_ARM64 + embedded_metadata[EMBED_METADATA_SIZE] = "NSMDSectionHeaderARM"; +#else + embedded_metadata[EMBED_METADATA_SIZE] = "NSMDSectionHeaderX86"; +#endif + +#endif // EMBED_METADATA_SIZE diff --git a/NativeScript/ffi/shared/direct/NativeApiDirect.h b/NativeScript/ffi/shared/direct/NativeApiDirect.h new file mode 100644 index 00000000..fd002fbb --- /dev/null +++ b/NativeScript/ffi/shared/direct/NativeApiDirect.h @@ -0,0 +1,29 @@ +#ifndef NATIVESCRIPT_FFI_SHARED_DIRECT_NATIVE_API_DIRECT_H +#define NATIVESCRIPT_FFI_SHARED_DIRECT_NATIVE_API_DIRECT_H + +#include +#include + +namespace nativescript { + +class NativeApiDirectScheduler { + public: + virtual ~NativeApiDirectScheduler() = default; + virtual void invokeOnJS(std::function task) = 0; + virtual void invokeOnUI(std::function task) = 0; +}; + +struct NativeApiDirectConfig { + 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)> jsThreadCallbackInvoker = nullptr; + bool installGlobalSymbols = false; +}; + +} // namespace nativescript + +#endif // NATIVESCRIPT_FFI_SHARED_DIRECT_NATIVE_API_DIRECT_H diff --git a/NativeScript/ffi/shared/jsi/NativeApiJsiBridge.h b/NativeScript/ffi/shared/jsi/NativeApiJsiBridge.h new file mode 100644 index 00000000..6485c374 --- /dev/null +++ b/NativeScript/ffi/shared/jsi/NativeApiJsiBridge.h @@ -0,0 +1,1629 @@ +thread_local bool gDispatchNativeCallsToUI = false; +thread_local bool gExecutingDispatchedUINativeCall = false; +thread_local int gSynchronousNativeInvocationDepth = 0; +thread_local int gNativeCallerThreadJsiCallbackDepth = 0; +std::atomic gActiveSynchronousNativeInvocationDepth{0}; + +class ScopedNativeApiUINativeCallDispatch final { + public: + ScopedNativeApiUINativeCallDispatch() + : previous_(gDispatchNativeCallsToUI) { + gDispatchNativeCallsToUI = true; + } + + ~ScopedNativeApiUINativeCallDispatch() { + gDispatchNativeCallsToUI = previous_; + } + + private: + bool previous_ = false; +}; + +bool shouldDispatchNativeCallToUI() { + return gDispatchNativeCallsToUI && ![NSThread isMainThread]; +} + +class ScopedNativeApiSynchronousInvocation final { + public: + ScopedNativeApiSynchronousInvocation() { + gSynchronousNativeInvocationDepth += 1; + gActiveSynchronousNativeInvocationDepth.fetch_add(1, + std::memory_order_acq_rel); + } + + ~ScopedNativeApiSynchronousInvocation() { + gSynchronousNativeInvocationDepth -= 1; + gActiveSynchronousNativeInvocationDepth.fetch_sub(1, + std::memory_order_acq_rel); + } +}; + +class ScopedNativeCallerThreadJsiCallback final { + public: + ScopedNativeCallerThreadJsiCallback() { + gNativeCallerThreadJsiCallbackDepth += 1; + } + + ~ScopedNativeCallerThreadJsiCallback() { + gNativeCallerThreadJsiCallbackDepth -= 1; + } + + ScopedNativeCallerThreadJsiCallback( + const ScopedNativeCallerThreadJsiCallback&) = delete; + ScopedNativeCallerThreadJsiCallback& operator=( + const ScopedNativeCallerThreadJsiCallback&) = delete; +}; + +template +void performNativeInvocation(Runtime& runtime, + const std::function)>& + invoker, + Invocation&& invocation) { + NSString* exceptionDescription = nil; + auto run = [&]() { + ScopedNativeApiSynchronousInvocation synchronousInvocation; + @try { + invocation(); + } @catch (NSException* exception) { + exceptionDescription = [exception.description copy]; + } + }; + + bool skipInvoker = gNativeCallerThreadJsiCallbackDepth > 0; + if (shouldDispatchNativeCallToUI()) { + dispatch_sync(dispatch_get_main_queue(), ^{ + bool previous = gExecutingDispatchedUINativeCall; + gExecutingDispatchedUINativeCall = true; + if (invoker && !skipInvoker) { + invoker(run); + } else { + run(); + } + gExecutingDispatchedUINativeCall = previous; + }); + } else if (invoker && !skipInvoker) { + invoker(run); + } else { + run(); + } + + if (exceptionDescription != nil) { + std::string message = exceptionDescription.UTF8String ?: ""; + [exceptionDescription release]; + throw facebook::jsi::JSError(runtime, message); + } +} + +enum class NativeApiSymbolKind { + Class, + Function, + Constant, + Protocol, + Enum, + Struct, + Union, +}; + +struct NativeApiSymbol { + NativeApiSymbolKind kind; + MDSectionOffset offset = 0; + MDSectionOffset superclassOffset = MD_SECTION_OFFSET_NULL; + std::string name; + std::string runtimeName; +}; + +struct NativeApiMember { + std::string name; + std::string selectorName; + std::string setterSelectorName; + MDSectionOffset signatureOffset = MD_SECTION_OFFSET_NULL; + MDSectionOffset setterSignatureOffset = MD_SECTION_OFFSET_NULL; + MDMemberFlag flags = metagen::mdMemberFlagNull; + bool property = false; + bool readonly = false; +}; + +struct NativeApiJsiAggregateInfo; + +struct NativeApiJsiFfiType { + ffi_type type = {}; + std::vector elements; + + NativeApiJsiFfiType() { + type.type = FFI_TYPE_STRUCT; + type.size = 0; + type.alignment = 0; + type.elements = nullptr; + } + + void finalize() { + elements.push_back(nullptr); + type.elements = elements.data(); + } +}; + +struct NativeApiJsiType { + MDTypeKind kind = metagen::mdTypeVoid; + ffi_type* ffiType = &ffi_type_void; + bool supported = true; + bool returnOwned = false; + MDSectionOffset signatureOffset = MD_SECTION_OFFSET_NULL; + MDSectionOffset aggregateOffset = MD_SECTION_OFFSET_NULL; + bool aggregateIsUnion = false; + uint16_t arraySize = 0; + std::shared_ptr elementType; + std::shared_ptr aggregateInfo; + std::shared_ptr ownedFfiType; +}; + +struct NativeApiJsiAggregateField { + std::string name; + uint16_t offset = 0; + NativeApiJsiType type; +}; + +struct NativeApiJsiAggregateInfo { + std::string name; + uint16_t size = 0; + bool isUnion = false; + MDSectionOffset offset = MD_SECTION_OFFSET_NULL; + std::vector fields; + std::shared_ptr ffi; +}; + +std::string jsifySelector(const char* selector) { + std::string jsifiedSelector; + bool nextUpper = false; + for (const char* c = selector; c != nullptr && *c != '\0'; c++) { + if (*c == ':') { + nextUpper = true; + } else if (nextUpper) { + jsifiedSelector += static_cast(toupper(*c)); + nextUpper = false; + } else { + jsifiedSelector += *c; + } + } + return jsifiedSelector; +} + +std::string booleanGetterSelectorForProperty(const std::string& property) { + if (property.empty()) { + return property; + } + + std::string selector = "is"; + selector += static_cast(toupper(property[0])); + selector += property.substr(1); + return selector; +} + +std::optional runtimeBooleanGetterSelectorForProperty( + Class cls, bool staticMethod, const std::string& property) { + if (cls == nil || property.empty()) { + return std::nullopt; + } + + std::string selectorName = booleanGetterSelectorForProperty(property); + SEL selector = sel_getUid(selectorName.c_str()); + if ((!staticMethod && class_getInstanceMethod(cls, selector) != nullptr) || + (staticMethod && class_getClassMethod(cls, selector) != nullptr)) { + return selectorName; + } + return std::nullopt; +} + +std::optional runtimeSelectorNameForProperty( + Class cls, bool staticMethod, const std::string& property) { + if (cls == nil || property.empty()) { + return std::nullopt; + } + +#if TARGET_OS_OSX + if (property == "initWithRedGreenBlueAlpha") { + const char* candidates[] = { + "initWithSRGBRed:green:blue:alpha:", + "initWithCalibratedRed:green:blue:alpha:", + }; + for (const char* candidate : candidates) { + SEL selector = sel_getUid(candidate); + if ((!staticMethod && class_getInstanceMethod(cls, selector) != nullptr) || + (staticMethod && class_getClassMethod(cls, selector) != nullptr)) { + return std::string(candidate); + } + } + } else if (property == "colorWithRedGreenBlueAlpha") { + const char* candidates[] = { + "colorWithSRGBRed:green:blue:alpha:", + "colorWithCalibratedRed:green:blue:alpha:", + }; + for (const char* candidate : candidates) { + SEL selector = sel_getUid(candidate); + if ((!staticMethod && class_getInstanceMethod(cls, selector) != nullptr) || + (staticMethod && class_getClassMethod(cls, selector) != nullptr)) { + return std::string(candidate); + } + } + } +#endif + + if (auto selectorName = + runtimeBooleanGetterSelectorForProperty(cls, staticMethod, property)) { + return selectorName; + } + + Class scan = staticMethod ? object_getClass(cls) : cls; + while (scan != Nil) { + unsigned int methodCount = 0; + Method* methods = class_copyMethodList(scan, &methodCount); + for (unsigned int i = 0; i < methodCount; i++) { + SEL selector = method_getName(methods[i]); + const char* selectorName = selector != nullptr ? sel_getName(selector) : nullptr; + if (selectorName != nullptr && + (property == selectorName || jsifySelector(selectorName) == property)) { + std::string result(selectorName); + free(methods); + return result; + } + } + free(methods); + scan = class_getSuperclass(scan); + } + + return std::nullopt; +} + +std::string setterSelectorForProperty(const std::string& property) { + if (property.empty()) { + return property; + } + + std::string selector = "set"; + selector += static_cast(toupper(property[0])); + selector += property.substr(1); + selector += ":"; + return selector; +} + +bool hasRuntimeSetterForProperty(Class cls, bool staticMethod, + const std::string& property) { + if (cls == nil || property.empty()) { + return false; + } + + std::string setterSelectorName = setterSelectorForProperty(property); + SEL selector = sel_getUid(setterSelectorName.c_str()); + return staticMethod ? class_getClassMethod(cls, selector) != nullptr + : class_getInstanceMethod(cls, selector) != nullptr; +} + +size_t selectorArgumentCount(const std::string& selector) { + return static_cast( + std::count(selector.begin(), selector.end(), ':')); +} + +const NativeApiMember* selectMethodMember( + const std::vector& members, const std::string& property, + bool staticMethod, size_t argumentCount) { + const NativeApiMember* fallback = nullptr; + for (const auto& member : members) { + if (member.property || member.name != property) { + continue; + } + + bool memberIsStatic = (member.flags & metagen::mdMemberStatic) != 0; + if (memberIsStatic != staticMethod) { + continue; + } + + if (fallback == nullptr) { + fallback = &member; + } + if (selectorArgumentCount(member.selectorName) == argumentCount) { + return &member; + } + } + return fallback; +} + +const NativeApiMember* selectPropertyMember( + const std::vector& members, const std::string& property, + bool staticMethod) { + for (const auto& member : members) { + if (!member.property || member.name != property) { + continue; + } + + bool memberIsStatic = (member.flags & metagen::mdMemberStatic) != 0; + if (memberIsStatic == staticMethod) { + return &member; + } + } + return nullptr; +} + +const NativeApiMember* selectWritablePropertyMember( + const std::vector& members, const std::string& property, + bool staticMethod) { + const NativeApiMember* fallback = nullptr; + for (const auto& member : members) { + if (!member.property || member.name != property) { + continue; + } + + bool memberIsStatic = (member.flags & metagen::mdMemberStatic) != 0; + if (memberIsStatic != staticMethod) { + continue; + } + + if (fallback == nullptr) { + fallback = &member; + } + if (!member.readonly && !member.setterSelectorName.empty()) { + return &member; + } + } + return fallback; +} + +void skipMetadataJsiType(MDMetadataReader* metadata, MDSectionOffset* offset); +Protocol* lookupProtocolByNativeName(const std::string& name); + +inline uintptr_t normalizeRuntimePointer(uintptr_t pointer) { +#if INTPTR_MAX == INT64_MAX + return pointer & 0x0000FFFFFFFFFFFFULL; +#else + return pointer; +#endif +} + +class NativeApiJsiBridge { + public: + explicit NativeApiJsiBridge(const NativeApiJsiConfig& config) + : metadata_(loadMetadata(config)), + scheduler_(config.scheduler), + nativeInvocationInvoker_(config.nativeInvocationInvoker), + nativeCallbackInvoker_(config.nativeCallbackInvoker), + jsThreadCallbackInvoker_(config.jsThreadCallbackInvoker) { + selfDl_ = dlopen(nullptr, RTLD_NOW); + buildSymbolIndexes(); + } + + ~NativeApiJsiBridge() { + if (selfDl_ != nullptr) { + dlclose(selfDl_); + } + } + + MDMetadataReader* metadata() const { return metadata_.get(); } + + void* selfDl() const { return selfDl_; } + + const NativeApiSymbol* find(const std::string& name) const { + auto it = symbolsByName_.find(name); + return it != symbolsByName_.end() ? &it->second : nullptr; + } + + const NativeApiSymbol* findClass(const std::string& name) const { + const NativeApiSymbol* symbol = find(name); + if (symbol != nullptr && symbol->kind == NativeApiSymbolKind::Class) { + return symbol; + } + auto it = classSymbolsByRuntimeName_.find(name); + return it != classSymbolsByRuntimeName_.end() ? &it->second : nullptr; + } + + const NativeApiSymbol* findClassByOffset(MDSectionOffset offset) const { + auto it = classSymbolsByOffset_.find(offset); + return it != classSymbolsByOffset_.end() ? &it->second : nullptr; + } + + const NativeApiSymbol* findClassForRuntimeClass(Class cls) const { + Class current = cls; + while (current != Nil) { + const char* name = class_getName(current); + if (name != nullptr) { + if (const NativeApiSymbol* symbol = findClass(name)) { + return symbol; + } + } + current = class_getSuperclass(current); + } + return nullptr; + } + + const NativeApiSymbol* findClassForRuntimePointer(void* pointer) const { + if (pointer == nullptr) { + return nullptr; + } + + auto it = classSymbolsByRuntimePointer_.find( + normalizeRuntimePointer(reinterpret_cast(pointer))); + return it != classSymbolsByRuntimePointer_.end() ? &it->second : nullptr; + } + + const NativeApiSymbol* findProtocolForRuntimePointer(void* pointer) const { + if (pointer == nullptr) { + return nullptr; + } + + auto it = protocolSymbolsByRuntimePointer_.find( + normalizeRuntimePointer(reinterpret_cast(pointer))); + return it != protocolSymbolsByRuntimePointer_.end() ? &it->second : nullptr; + } + + const NativeApiSymbol* findFunction(const std::string& name) const { + auto it = functionSymbolsByName_.find(name); + return it != functionSymbolsByName_.end() ? &it->second : nullptr; + } + + void rememberRoundTripValue(Runtime& runtime, const void* native, + const Value& value) { + if (native == nullptr) { + return; + } + roundTripValues_[normalizeRuntimePointer( + reinterpret_cast(native))] = + std::make_shared(runtime, value); + } + + Value findRoundTripValue(Runtime& runtime, const void* native) const { + if (native == nullptr) { + return Value::undefined(); + } + auto it = roundTripValues_.find( + normalizeRuntimePointer(reinterpret_cast(native))); + if (it == roundTripValues_.end() || it->second == nullptr) { + return Value::undefined(); + } + return Value(runtime, *it->second); + } + + void forgetRoundTripValue(const void* native) { + if (native == nullptr) { + return; + } + roundTripValues_.erase( + normalizeRuntimePointer(reinterpret_cast(native))); + } + + void rememberClassValue(Runtime& runtime, Class cls, const Value& value) { + if (cls == Nil) { + return; + } + classValues_[normalizeRuntimePointer(reinterpret_cast(cls))] = + std::make_shared(runtime, value); + } + + Value findClassValue(Runtime& runtime, Class cls) const { + if (cls == Nil) { + return Value::undefined(); + } + auto it = classValues_.find( + normalizeRuntimePointer(reinterpret_cast(cls))); + if (it == classValues_.end() || it->second == nullptr) { + return Value::undefined(); + } + return Value(runtime, *it->second); + } + + void rememberClassPrototype(Runtime& runtime, Class cls, const Value& value) { + if (cls == Nil) { + return; + } + classPrototypes_[normalizeRuntimePointer(reinterpret_cast(cls))] = + std::make_shared(runtime, value); + } + + Value findClassPrototype(Runtime& runtime, Class cls) const { + if (cls == Nil) { + return Value::undefined(); + } + auto it = classPrototypes_.find( + normalizeRuntimePointer(reinterpret_cast(cls))); + if (it == classPrototypes_.end() || it->second == nullptr) { + return Value::undefined(); + } + return Value(runtime, *it->second); + } + + void setObjectExpando(Runtime& runtime, const void* native, + const std::string& property, const Value& value) { + if (native == nullptr || property.empty()) { + return; + } + objectExpandos_[normalizeRuntimePointer(reinterpret_cast(native))] + [property] = std::make_shared(runtime, value); + } + + Value findObjectExpando(Runtime& runtime, const void* native, + const std::string& property) const { + if (native == nullptr || property.empty()) { + return Value::undefined(); + } + auto objectIt = objectExpandos_.find( + normalizeRuntimePointer(reinterpret_cast(native))); + if (objectIt == objectExpandos_.end()) { + return Value::undefined(); + } + auto propertyIt = objectIt->second.find(property); + if (propertyIt == objectIt->second.end() || propertyIt->second == nullptr) { + return Value::undefined(); + } + return Value(runtime, *propertyIt->second); + } + + void forgetObjectExpandos(const void* native) { + if (native == nullptr) { + return; + } + objectExpandos_.erase( + normalizeRuntimePointer(reinterpret_cast(native))); + } + + void rememberPointerValue(Runtime& runtime, const void* native, + const Value& value) { + pointerValues_[reinterpret_cast(native)] = + std::make_shared(runtime, value); + } + + Value findPointerValue(Runtime& runtime, const void* native) const { + auto it = pointerValues_.find(reinterpret_cast(native)); + if (it == pointerValues_.end() || it->second == nullptr) { + return Value::undefined(); + } + return Value(runtime, *it->second); + } + + void forgetPointerValue(const void* native) { + if (native == nullptr) { + return; + } + pointerValues_.erase(reinterpret_cast(native)); + } + + const NativeApiSymbol* findConstant(const std::string& name) const { + auto it = constantSymbolsByName_.find(name); + return it != constantSymbolsByName_.end() ? &it->second : nullptr; + } + + const NativeApiSymbol* findProtocol(const std::string& name) const { + const NativeApiSymbol* symbol = find(name); + if (symbol != nullptr && symbol->kind == NativeApiSymbolKind::Protocol) { + return symbol; + } + auto it = protocolSymbolsByRuntimeName_.find(name); + return it != protocolSymbolsByRuntimeName_.end() ? &it->second : nullptr; + } + + const NativeApiSymbol* findEnum(const std::string& name) const { + auto it = enumSymbolsByName_.find(name); + return it != enumSymbolsByName_.end() ? &it->second : nullptr; + } + + const NativeApiSymbol* findStruct(const std::string& name) const { + auto it = structSymbolsByName_.find(name); + return it != structSymbolsByName_.end() ? &it->second : nullptr; + } + + const NativeApiSymbol* findUnion(const std::string& name) const { + auto it = unionSymbolsByName_.find(name); + return it != unionSymbolsByName_.end() ? &it->second : nullptr; + } + + const NativeApiSymbol* findAggregate(const std::string& name) const { + const NativeApiSymbol* symbol = findStruct(name); + if (symbol != nullptr) { + return symbol; + } + return findUnion(name); + } + + size_t classCount() const { return classNames_.size(); } + size_t functionCount() const { return functionNames_.size(); } + size_t constantCount() const { return constantNames_.size(); } + size_t protocolCount() const { return protocolNames_.size(); } + size_t enumCount() const { return enumNames_.size(); } + size_t structCount() const { return structNames_.size(); } + size_t unionCount() const { return unionNames_.size(); } + + const std::vector& classNames() const { return classNames_; } + const std::vector& functionNames() const { return functionNames_; } + const std::vector& constantNames() const { return constantNames_; } + const std::vector& protocolNames() const { return protocolNames_; } + const std::vector& enumNames() const { return enumNames_; } + const std::vector& structNames() const { return structNames_; } + const std::vector& unionNames() const { return unionNames_; } + std::shared_ptr scheduler() const { return scheduler_; } + const std::function)>& nativeInvocationInvoker() + const { + return nativeInvocationInvoker_; + } + const std::function)>& nativeCallbackInvoker() + const { + return nativeCallbackInvoker_; + } + const std::function)>& jsThreadCallbackInvoker() + const { + return jsThreadCallbackInvoker_; + } + std::thread::id jsThreadId() const { return jsThreadId_; } + + void retainJsiLifetime(std::shared_ptr lifetime) { + if (lifetime == nullptr) { + return; + } + std::lock_guard lock(retainedLifetimesMutex_); + retainedLifetimes_.push_back(std::move(lifetime)); + } + + const std::vector& membersForClass( + const NativeApiSymbol& symbol) const { + auto cached = membersByClassOffset_.find(symbol.offset); + if (cached != membersByClassOffset_.end()) { + return cached->second; + } + + auto inserted = membersByClassOffset_.emplace( + symbol.offset, readMembersForClassHierarchy(symbol)); + return inserted.first->second; + } + + const std::vector& surfaceMembersForClass( + const NativeApiSymbol& symbol) const { + auto cached = surfaceMembersByClassOffset_.find(symbol.offset); + if (cached != surfaceMembersByClassOffset_.end()) { + return cached->second; + } + + auto inserted = surfaceMembersByClassOffset_.emplace( + symbol.offset, readSurfaceMembersForClass(symbol)); + return inserted.first->second; + } + + const std::vector& membersForProtocol( + const NativeApiSymbol& symbol) const { + auto cached = membersByProtocolOffset_.find(symbol.offset); + if (cached != membersByProtocolOffset_.end()) { + return cached->second; + } + + auto inserted = membersByProtocolOffset_.emplace( + symbol.offset, readMembersForProtocolHierarchy(symbol.offset)); + return inserted.first->second; + } + + std::shared_ptr aggregateInfoFor( + MDSectionOffset aggregateOffset, bool isUnion); + + std::shared_ptr aggregateInfoFor( + const NativeApiSymbol& symbol) { + return aggregateInfoFor(symbol.offset, + symbol.kind == NativeApiSymbolKind::Union); + } + + private: + static std::unique_ptr loadMetadataFromFile( + const char* metadataPath) { + const char* path = metadataPath != nullptr ? metadataPath : "metadata.nsmd"; + FILE* file = fopen(path, "rb"); + if (file == nullptr) { + throw std::runtime_error(std::string("metadata.nsmd not found: ") + path); + } + + fseek(file, 0, SEEK_END); + long size = ftell(file); + fseek(file, 0, SEEK_SET); + if (size <= 0) { + fclose(file); + throw std::runtime_error(std::string("metadata.nsmd is empty: ") + path); + } + + void* buffer = malloc(static_cast(size)); + if (buffer == nullptr) { + fclose(file); + throw std::bad_alloc(); + } + + size_t read = fread(buffer, 1, static_cast(size), file); + fclose(file); + if (read != static_cast(size)) { + free(buffer); + throw std::runtime_error(std::string("failed to read metadata: ") + path); + } + + return std::make_unique(buffer, true); + } + + static std::unique_ptr loadMetadata( + const NativeApiJsiConfig& config) { + if (config.metadataPtr != nullptr && + *static_cast(config.metadataPtr) != '\0') { +#ifdef EMBED_METADATA_SIZE + return std::make_unique((void*)embedded_metadata); +#else + return std::make_unique( + const_cast(config.metadataPtr)); +#endif + } + +#ifdef EMBED_METADATA_SIZE + if (config.metadataPath == nullptr) { + return std::make_unique((void*)embedded_metadata); + } +#endif + + unsigned long segmentSize = 0; + auto segmentData = getsegmentdata( + reinterpret_cast(_dyld_get_image_header(0)), + "__objc_metadata", &segmentSize); + if (segmentData != nullptr && segmentSize > 0) { + return std::make_unique(segmentData); + } + + return loadMetadataFromFile(config.metadataPath); + } + + void addSymbol(NativeApiSymbolKind kind, MDSectionOffset offset, + const char* name, const char* runtimeName = nullptr, + MDSectionOffset superclassOffset = MD_SECTION_OFFSET_NULL) { + if (name == nullptr || name[0] == '\0') { + return; + } + + NativeApiSymbol symbol{ + .kind = kind, + .offset = offset, + .superclassOffset = superclassOffset, + .name = name, + .runtimeName = runtimeName != nullptr ? runtimeName : name, + }; + + switch (kind) { + case NativeApiSymbolKind::Class: + classNames_.push_back(symbol.name); + break; + case NativeApiSymbolKind::Function: + functionNames_.push_back(symbol.name); + functionSymbolsByName_[symbol.name] = symbol; + break; + case NativeApiSymbolKind::Constant: + constantNames_.push_back(symbol.name); + constantSymbolsByName_[symbol.name] = symbol; + break; + case NativeApiSymbolKind::Protocol: + protocolNames_.push_back(symbol.name); + break; + case NativeApiSymbolKind::Enum: + enumNames_.push_back(symbol.name); + enumSymbolsByName_[symbol.name] = symbol; + break; + case NativeApiSymbolKind::Struct: + structNames_.push_back(symbol.name); + structSymbolsByName_[symbol.name] = symbol; + break; + case NativeApiSymbolKind::Union: + unionNames_.push_back(symbol.name); + unionSymbolsByName_[symbol.name] = symbol; + break; + } + + symbolsByName_[symbol.name] = symbol; + if (kind == NativeApiSymbolKind::Class) { + classSymbolsByOffset_[symbol.offset] = symbol; + classSymbolsByRuntimeName_[symbol.runtimeName] = symbol; + Class cls = objc_lookUpClass(symbol.runtimeName.c_str()); + if (cls != Nil) { + classSymbolsByRuntimePointer_[normalizeRuntimePointer( + reinterpret_cast(cls))] = symbol; + } + } else if (kind == NativeApiSymbolKind::Protocol) { + protocolSymbolsByOffset_[symbol.offset] = symbol; + protocolSymbolsByRuntimeName_[symbol.runtimeName] = symbol; + auto rememberProtocolRuntimeName = [&](const std::string& runtimeName) { + if (runtimeName.empty()) { + return; + } + protocolSymbolsByRuntimeName_[runtimeName] = symbol; + Protocol* runtimeProtocol = lookupProtocolByNativeName(runtimeName); + if (runtimeProtocol != nullptr) { + protocolSymbolsByRuntimePointer_[normalizeRuntimePointer( + reinterpret_cast(runtimeProtocol))] = symbol; + } + }; + if (symbol.name.size() > 9 && + std::isdigit(static_cast(symbol.name.back()))) { + size_t digitsStart = symbol.name.size(); + while (digitsStart > 0 && + std::isdigit(static_cast(symbol.name[digitsStart - 1]))) { + digitsStart--; + } + constexpr const char* protocolSuffix = "Protocol"; + size_t protocolSuffixLength = std::strlen(protocolSuffix); + if (digitsStart > protocolSuffixLength && + symbol.name.compare(digitsStart - protocolSuffixLength, + protocolSuffixLength, protocolSuffix) == 0) { + rememberProtocolRuntimeName( + symbol.name.substr(0, digitsStart - protocolSuffixLength)); + } + } + Protocol* protocol = lookupProtocolByNativeName(symbol.runtimeName); + if (protocol == nullptr && symbol.runtimeName != symbol.name) { + protocol = lookupProtocolByNativeName(symbol.name); + } + if (protocol != nullptr) { + protocolSymbolsByRuntimePointer_[normalizeRuntimePointer( + reinterpret_cast(protocol))] = symbol; + } + } else if (kind == NativeApiSymbolKind::Struct) { + structSymbolsByOffset_[symbol.offset] = symbol; + } else if (kind == NativeApiSymbolKind::Union) { + unionSymbolsByOffset_[symbol.offset] = symbol; + } + } + + void addAggregateAliases(NativeApiSymbolKind kind, MDSectionOffset offset, + const std::string& name) { + if (name.empty()) { + return; + } + + if (!name.empty() && name[0] == '_') { + std::string alias = name.substr(1); + if (!alias.empty() && symbolsByName_.find(alias) == symbolsByName_.end()) { + addSymbol(kind, offset, alias.c_str(), name.c_str()); + } + } + + constexpr const char* suffix = "Struct"; + if (name.size() < std::strlen(suffix) || + name.compare(name.size() - std::strlen(suffix), std::strlen(suffix), + suffix) != 0) { + std::string alias = name + suffix; + if (symbolsByName_.find(alias) == symbolsByName_.end()) { + addSymbol(kind, offset, alias.c_str(), name.c_str()); + } + } + } + + void buildSymbolIndexes() { + if (metadata_ == nullptr) { + return; + } + + indexConstants(); + indexEnums(); + indexFunctions(); + indexProtocols(); + indexClasses(); + indexStructs(); + indexUnions(); + } + + static void skipConstantValue(MDMetadataReader* metadata, + MDSectionOffset& offset, + metagen::MDVariableEvalKind evalKind) { + switch (evalKind) { + case metagen::mdEvalNone: + skipMetadataJsiType(metadata, &offset); + break; + case metagen::mdEvalInt64: + offset += sizeof(int64_t); + break; + case metagen::mdEvalDouble: + offset += sizeof(double); + break; + case metagen::mdEvalString: + offset += sizeof(MDSectionOffset); + break; + } + } + + void indexConstants() { + MDSectionOffset offset = metadata_->constantsOffset; + while (offset < metadata_->enumsOffset) { + MDSectionOffset originalOffset = offset; + addSymbol(NativeApiSymbolKind::Constant, originalOffset, + metadata_->getString(offset)); + offset += sizeof(MDSectionOffset); + auto evalKind = metadata_->getVariableEvalKind(offset); + offset += sizeof(metagen::MDVariableEvalKind); + skipConstantValue(metadata_.get(), offset, evalKind); + } + } + + void indexEnums() { + MDSectionOffset offset = metadata_->enumsOffset; + while (offset < metadata_->signaturesOffset) { + MDSectionOffset originalOffset = offset; + addSymbol(NativeApiSymbolKind::Enum, originalOffset, + metadata_->getString(offset)); + offset += sizeof(MDSectionOffset); + + bool next = true; + while (next) { + auto nameOffset = metadata_->getOffset(offset); + next = (nameOffset & metagen::mdSectionOffsetNext) != 0; + offset += sizeof(MDSectionOffset); + offset += sizeof(int64_t); + } + } + } + + void indexFunctions() { + MDSectionOffset offset = metadata_->functionsOffset; + while (offset < metadata_->protocolsOffset) { + MDSectionOffset originalOffset = offset; + addSymbol(NativeApiSymbolKind::Function, originalOffset, + metadata_->getString(offset)); + offset += sizeof(MDSectionOffset); + offset += sizeof(MDSectionOffset); + offset += sizeof(metagen::MDFunctionFlag); + } + } + + void indexProtocols() { + MDSectionOffset offset = metadata_->protocolsOffset; + while (offset < metadata_->classesOffset) { + MDSectionOffset originalOffset = offset; + auto nameOffset = metadata_->getOffset(offset); + offset += sizeof(MDSectionOffset); + bool next = (nameOffset & metagen::mdSectionOffsetNext) != 0; + nameOffset &= ~metagen::mdSectionOffsetNext; + addSymbol(NativeApiSymbolKind::Protocol, originalOffset, + metadata_->resolveString(nameOffset)); + + while (next) { + auto protocolOffset = metadata_->getOffset(offset); + offset += sizeof(MDSectionOffset); + next = (protocolOffset & metagen::mdSectionOffsetNext) != 0; + } + + next = true; + while (next) { + auto flags = metadata_->getMemberFlag(offset); + next = (flags & metagen::mdMemberNext) != 0; + offset += sizeof(flags); + if (flags == metagen::mdMemberFlagNull) { + break; + } + + skipMember(flags, offset); + } + } + } + + void indexClasses() { + MDSectionOffset offset = metadata_->classesOffset; + while (offset < metadata_->structsOffset) { + MDSectionOffset originalOffset = offset; + auto nameOffset = metadata_->getOffset(offset); + offset += sizeof(MDSectionOffset); + auto runtimeNameOffset = metadata_->getOffset(offset); + offset += sizeof(MDSectionOffset); + bool hasProtocols = (nameOffset & metagen::mdSectionOffsetNext) != 0; + nameOffset &= ~metagen::mdSectionOffsetNext; + + auto name = metadata_->resolveString(nameOffset); + const char* runtimeName = name; + if (runtimeNameOffset != MD_SECTION_OFFSET_NULL) { + runtimeName = metadata_->resolveString(runtimeNameOffset); + } + + while (hasProtocols) { + auto protocolOffset = metadata_->getOffset(offset); + offset += sizeof(MDSectionOffset); + hasProtocols = (protocolOffset & metagen::mdSectionOffsetNext) != 0; + } + + auto superclass = metadata_->getOffset(offset); + offset += sizeof(superclass); + MDSectionOffset superclassOffset = + superclass & ~metagen::mdSectionOffsetNext; + if (superclassOffset != MD_SECTION_OFFSET_NULL) { + superclassOffset += metadata_->classesOffset; + } + + addSymbol(NativeApiSymbolKind::Class, originalOffset, name, runtimeName, + superclassOffset); + + bool next = (superclass & metagen::mdSectionOffsetNext) != 0; + while (next) { + auto flags = metadata_->getMemberFlag(offset); + next = (flags & metagen::mdMemberNext) != 0; + offset += sizeof(flags); + skipMember(flags, offset); + } + } + } + + void skipAggregateFields(MDSectionOffset& offset, bool isUnion) const { + bool next = true; + while (next) { + MDSectionOffset nameOffset = metadata_->getOffset(offset); + offset += sizeof(MDSectionOffset); + next = (nameOffset & metagen::mdSectionOffsetNext) != 0; + nameOffset &= ~metagen::mdSectionOffsetNext; + if (nameOffset == MD_SECTION_OFFSET_NULL) { + break; + } + if (!isUnion) { + offset += sizeof(uint16_t); + } + skipMetadataJsiType(metadata_.get(), &offset); + } + } + + void indexStructs() { + MDSectionOffset offset = metadata_->structsOffset; + while (offset < metadata_->unionsOffset) { + if (metadata_->getOffset(offset) == 0) { + break; + } + MDSectionOffset originalOffset = offset; + const char* name = metadata_->getString(offset); + offset += sizeof(MDSectionOffset); + offset += sizeof(uint16_t); + addSymbol(NativeApiSymbolKind::Struct, originalOffset, name); + addAggregateAliases(NativeApiSymbolKind::Struct, originalOffset, + name != nullptr ? name : ""); + skipAggregateFields(offset, false); + } + } + + void indexUnions() { + MDSectionOffset offset = metadata_->unionsOffset; + while (metadata_->getOffset(offset) != 0) { + MDSectionOffset originalOffset = offset; + const char* name = metadata_->getString(offset); + offset += sizeof(MDSectionOffset); + offset += sizeof(uint16_t); + addSymbol(NativeApiSymbolKind::Union, originalOffset, name); + addAggregateAliases(NativeApiSymbolKind::Union, originalOffset, + name != nullptr ? name : ""); + skipAggregateFields(offset, true); + } + } + + void skipMember(MDMemberFlag flags, MDSectionOffset& offset) const { + if ((flags & metagen::mdMemberProperty) != 0) { + bool readonly = (flags & metagen::mdMemberReadonly) != 0; + offset += sizeof(MDSectionOffset); + offset += sizeof(MDSectionOffset); + offset += sizeof(MDSectionOffset); + if (!readonly) { + offset += sizeof(MDSectionOffset); + offset += sizeof(MDSectionOffset); + } + return; + } + + offset += sizeof(MDSectionOffset); + offset += sizeof(MDSectionOffset); + } + + std::vector readProtocolOffsetsForClass( + MDSectionOffset classOffset, MDSectionOffset* memberOffset = nullptr, + MDSectionOffset* superclassOffsetOut = nullptr) const { + std::vector protocols; + if (metadata_ == nullptr || classOffset == MD_SECTION_OFFSET_NULL) { + return protocols; + } + + MDSectionOffset offset = classOffset; + auto nameOffset = metadata_->getOffset(offset); + offset += sizeof(MDSectionOffset); + offset += sizeof(MDSectionOffset); + bool hasProtocols = (nameOffset & metagen::mdSectionOffsetNext) != 0; + + while (hasProtocols) { + auto protocolOffset = metadata_->getOffset(offset); + offset += sizeof(MDSectionOffset); + hasProtocols = (protocolOffset & metagen::mdSectionOffsetNext) != 0; + protocolOffset &= ~metagen::mdSectionOffsetNext; + if (protocolOffset != MD_SECTION_OFFSET_NULL) { + protocols.push_back(protocolOffset + metadata_->protocolsOffset); + } + } + + auto superclass = metadata_->getOffset(offset); + offset += sizeof(superclass); + const bool hasMembers = (superclass & metagen::mdSectionOffsetNext) != 0; + if (superclassOffsetOut != nullptr) { + MDSectionOffset superclassOffset = + superclass & ~metagen::mdSectionOffsetNext; + *superclassOffsetOut = + superclassOffset != MD_SECTION_OFFSET_NULL + ? superclassOffset + metadata_->classesOffset + : MD_SECTION_OFFSET_NULL; + } + if (memberOffset != nullptr) { + *memberOffset = hasMembers ? offset : MD_SECTION_OFFSET_NULL; + } + return protocols; + } + + std::vector readOwnMembersForClass( + MDSectionOffset classOffset) const { + std::vector members; + if (metadata_ == nullptr || classOffset == MD_SECTION_OFFSET_NULL) { + return members; + } + + MDSectionOffset memberOffset = MD_SECTION_OFFSET_NULL; + for (MDSectionOffset protocolOffset : + readProtocolOffsetsForClass(classOffset, &memberOffset)) { + auto protocol = protocolSymbolsByOffset_.find(protocolOffset); + if (protocol == protocolSymbolsByOffset_.end()) { + continue; + } + const auto& protocolMembers = membersForProtocol(protocol->second); + members.insert(members.end(), protocolMembers.begin(), + protocolMembers.end()); + } + + if (memberOffset != MD_SECTION_OFFSET_NULL) { + std::vector ownMembers = + readMembersAtOffset(memberOffset); + members.insert(members.end(), ownMembers.begin(), ownMembers.end()); + } + return members; + } + + std::vector readMembersForClass( + MDSectionOffset classOffset) const { + std::vector members; + if (metadata_ == nullptr || classOffset == MD_SECTION_OFFSET_NULL) { + return members; + } + + MDSectionOffset offset = classOffset; + auto nameOffset = metadata_->getOffset(offset); + offset += sizeof(MDSectionOffset); + offset += sizeof(MDSectionOffset); + bool hasProtocols = (nameOffset & metagen::mdSectionOffsetNext) != 0; + + while (hasProtocols) { + auto protocolOffset = metadata_->getOffset(offset); + offset += sizeof(MDSectionOffset); + hasProtocols = (protocolOffset & metagen::mdSectionOffsetNext) != 0; + } + + auto superclass = metadata_->getOffset(offset); + offset += sizeof(superclass); + + bool next = (superclass & metagen::mdSectionOffsetNext) != 0; + while (next) { + auto flags = metadata_->getMemberFlag(offset); + next = (flags & metagen::mdMemberNext) != 0; + offset += sizeof(flags); + if (flags == metagen::mdMemberFlagNull) { + break; + } + + NativeApiMember member; + member.flags = flags; + if ((flags & metagen::mdMemberProperty) != 0) { + member.property = true; + member.readonly = (flags & metagen::mdMemberReadonly) != 0; + member.name = metadata_->getString(offset); + offset += sizeof(MDSectionOffset); + member.selectorName = metadata_->getString(offset); + offset += sizeof(MDSectionOffset); + member.signatureOffset = + metadata_->signaturesOffset + metadata_->getOffset(offset); + offset += sizeof(MDSectionOffset); + + if (!member.readonly) { + member.setterSelectorName = metadata_->getString(offset); + offset += sizeof(MDSectionOffset); + member.setterSignatureOffset = + metadata_->signaturesOffset + metadata_->getOffset(offset); + offset += sizeof(MDSectionOffset); + } + } else { + member.selectorName = metadata_->getString(offset); + offset += sizeof(MDSectionOffset); + member.signatureOffset = + metadata_->signaturesOffset + metadata_->getOffset(offset); + offset += sizeof(MDSectionOffset); + member.name = jsifySelector(member.selectorName.c_str()); + } + members.push_back(std::move(member)); + } + + return members; + } + + static bool memberIsStatic(const NativeApiMember& member) { + return (member.flags & metagen::mdMemberStatic) != 0; + } + + static bool sameMemberSlot(const NativeApiMember& lhs, + const NativeApiMember& rhs) { + return lhs.name == rhs.name && lhs.property == rhs.property && + memberIsStatic(lhs) == memberIsStatic(rhs); + } + + static bool sameMethodSelector(const NativeApiMember& lhs, + const NativeApiMember& rhs) { + return !lhs.property && !rhs.property && sameMemberSlot(lhs, rhs) && + lhs.selectorName == rhs.selectorName; + } + + static const NativeApiMember* findPropertyMember( + const std::vector& members, + const NativeApiMember& candidate) { + for (const auto& member : members) { + if (member.property && sameMemberSlot(member, candidate)) { + return &member; + } + } + return nullptr; + } + + static bool selectorExistsInMembers( + const std::vector& members, + const NativeApiMember& candidate) { + for (const auto& member : members) { + if (sameMethodSelector(member, candidate)) { + return true; + } + } + return false; + } + + static bool shouldSkipPropertyOverride( + const NativeApiMember* inherited, const NativeApiMember& member) { + if (inherited == nullptr || !inherited->property) { + return false; + } + + bool sameGetter = inherited->selectorName == member.selectorName; + bool sameSetter = + inherited->setterSelectorName == member.setterSelectorName; + if ((!inherited->readonly && member.readonly) || + (inherited->readonly == member.readonly && sameGetter && + (member.readonly || sameSetter))) { + return true; + } + return false; + } + + static void appendSurfaceMember( + std::vector& surface, + const std::vector& inheritedMembers, + const NativeApiMember& member) { + if (member.name.empty()) { + return; + } + + if (member.property) { + const NativeApiMember* inherited = + findPropertyMember(inheritedMembers, member); + if (shouldSkipPropertyOverride(inherited, member)) { + return; + } + + for (auto& existing : surface) { + if (!existing.property || !sameMemberSlot(existing, member)) { + continue; + } + if (existing.readonly && !member.readonly) { + existing = member; + } + return; + } + surface.push_back(member); + return; + } + + const bool keepInheritedMethod = + member.name == "alloc" || member.name == "toString" || + member.name == "superclass"; + if (!keepInheritedMethod && + selectorExistsInMembers(inheritedMembers, member)) { + return; + } + if (selectorExistsInMembers(surface, member)) { + return; + } + surface.push_back(member); + } + + std::vector readSurfaceMembersForClass( + const NativeApiSymbol& symbol) const { + std::vector inheritedMembers; + if (symbol.superclassOffset != MD_SECTION_OFFSET_NULL) { + auto superclass = classSymbolsByOffset_.find(symbol.superclassOffset); + if (superclass != classSymbolsByOffset_.end()) { + const auto& inherited = surfaceMembersForClass(superclass->second); + inheritedMembers.insert(inheritedMembers.end(), inherited.begin(), + inherited.end()); + } + } + + std::vector surface; + for (const auto& member : readOwnMembersForClass(symbol.offset)) { + appendSurfaceMember(surface, inheritedMembers, member); + } + return surface; + } + + std::vector readMembersAtOffset( + MDSectionOffset& offset) const { + std::vector members; + bool next = true; + while (next) { + auto flags = metadata_->getMemberFlag(offset); + next = (flags & metagen::mdMemberNext) != 0; + offset += sizeof(flags); + if (flags == metagen::mdMemberFlagNull) { + break; + } + + NativeApiMember member; + member.flags = flags; + if ((flags & metagen::mdMemberProperty) != 0) { + member.property = true; + member.readonly = (flags & metagen::mdMemberReadonly) != 0; + member.name = metadata_->getString(offset); + offset += sizeof(MDSectionOffset); + member.selectorName = metadata_->getString(offset); + offset += sizeof(MDSectionOffset); + member.signatureOffset = + metadata_->signaturesOffset + metadata_->getOffset(offset); + offset += sizeof(MDSectionOffset); + + if (!member.readonly) { + member.setterSelectorName = metadata_->getString(offset); + offset += sizeof(MDSectionOffset); + member.setterSignatureOffset = + metadata_->signaturesOffset + metadata_->getOffset(offset); + offset += sizeof(MDSectionOffset); + } + } else { + member.selectorName = metadata_->getString(offset); + offset += sizeof(MDSectionOffset); + member.signatureOffset = + metadata_->signaturesOffset + metadata_->getOffset(offset); + offset += sizeof(MDSectionOffset); + member.name = jsifySelector(member.selectorName.c_str()); + } + members.push_back(std::move(member)); + } + return members; + } + + std::vector readMembersForProtocolHierarchy( + MDSectionOffset protocolOffset) const { + std::vector members; + if (metadata_ == nullptr || protocolOffset == MD_SECTION_OFFSET_NULL) { + return members; + } + + MDSectionOffset offset = protocolOffset; + auto nameOffset = metadata_->getOffset(offset); + offset += sizeof(MDSectionOffset); + bool hasProtocols = (nameOffset & metagen::mdSectionOffsetNext) != 0; + + while (hasProtocols) { + auto inheritedOffset = metadata_->getOffset(offset); + offset += sizeof(MDSectionOffset); + hasProtocols = (inheritedOffset & metagen::mdSectionOffsetNext) != 0; + inheritedOffset &= ~metagen::mdSectionOffsetNext; + if (inheritedOffset == MD_SECTION_OFFSET_NULL) { + continue; + } + + MDSectionOffset absoluteOffset = + inheritedOffset + metadata_->protocolsOffset; + auto inheritedSymbol = protocolSymbolsByOffset_.find(absoluteOffset); + if (inheritedSymbol != protocolSymbolsByOffset_.end()) { + const auto& inheritedMembers = + membersForProtocol(inheritedSymbol->second); + members.insert(members.end(), inheritedMembers.begin(), + inheritedMembers.end()); + } + } + + std::vector ownMembers = readMembersAtOffset(offset); + members.insert(members.end(), ownMembers.begin(), ownMembers.end()); + return members; + } + + std::vector readMembersForClassHierarchy( + const NativeApiSymbol& symbol) const { + std::vector members = readOwnMembersForClass(symbol.offset); + if (symbol.superclassOffset == MD_SECTION_OFFSET_NULL) { + return members; + } + + auto superclass = classSymbolsByOffset_.find(symbol.superclassOffset); + if (superclass != classSymbolsByOffset_.end()) { + const auto& inheritedMembers = membersForClass(superclass->second); + members.insert(members.end(), inheritedMembers.begin(), + inheritedMembers.end()); + } + return members; + } + + std::unique_ptr metadata_; + void* selfDl_ = nullptr; + std::unordered_map symbolsByName_; + std::unordered_map functionSymbolsByName_; + std::unordered_map constantSymbolsByName_; + std::unordered_map enumSymbolsByName_; + std::unordered_map structSymbolsByName_; + std::unordered_map unionSymbolsByName_; + std::unordered_map classSymbolsByRuntimeName_; + std::unordered_map protocolSymbolsByRuntimeName_; + std::unordered_map classSymbolsByRuntimePointer_; + std::unordered_map protocolSymbolsByRuntimePointer_; + std::unordered_map> roundTripValues_; + std::unordered_map> classValues_; + std::unordered_map> classPrototypes_; + std::unordered_map> pointerValues_; + std::unordered_map>> + objectExpandos_; + std::unordered_map classSymbolsByOffset_; + std::unordered_map protocolSymbolsByOffset_; + std::vector classNames_; + std::vector functionNames_; + std::vector constantNames_; + std::vector protocolNames_; + std::vector enumNames_; + std::vector structNames_; + std::vector unionNames_; + std::shared_ptr scheduler_; + std::function)> nativeInvocationInvoker_; + std::function)> nativeCallbackInvoker_; + std::function)> jsThreadCallbackInvoker_; + mutable std::unordered_map> + membersByClassOffset_; + mutable std::unordered_map> + surfaceMembersByClassOffset_; + mutable std::unordered_map> + membersByProtocolOffset_; + std::unordered_map structSymbolsByOffset_; + std::unordered_map unionSymbolsByOffset_; + std::unordered_map> + aggregateInfoByOffset_; + std::unordered_set aggregateInfoInProgress_; + std::thread::id jsThreadId_ = std::this_thread::get_id(); + std::mutex retainedLifetimesMutex_; + std::vector> retainedLifetimes_; +}; + +Value makeString(Runtime& runtime, const std::string& value) { + return String::createFromUtf8(runtime, value); +} + +std::string readStringArg(Runtime& runtime, const Value* args, size_t count, + size_t index, const char* argumentName) { + if (index >= count || !args[index].isString()) { + throw facebook::jsi::JSError( + runtime, std::string(argumentName) + " must be a string."); + } + return args[index].asString(runtime).utf8(runtime); +} + +const char* kindName(NativeApiSymbolKind kind) { + switch (kind) { + case NativeApiSymbolKind::Class: + return "class"; + case NativeApiSymbolKind::Function: + return "function"; + case NativeApiSymbolKind::Constant: + return "constant"; + case NativeApiSymbolKind::Protocol: + return "protocol"; + case NativeApiSymbolKind::Enum: + return "enum"; + case NativeApiSymbolKind::Struct: + return "struct"; + case NativeApiSymbolKind::Union: + return "union"; + } + return "unknown"; +} + +Array namesToArray(Runtime& runtime, const std::vector& names) { + Array result(runtime, names.size()); + for (size_t i = 0; i < names.size(); i++) { + result.setValueAtIndex(runtime, i, makeString(runtime, names[i])); + } + return result; +} + +void addPropertyName(Runtime& runtime, std::vector& names, + const char* name) { + names.push_back(PropNameID::forAscii(runtime, name)); +} + +class NativeApiPointerHostObject; +class NativeApiObjectHostObject; +class NativeApiClassHostObject; +class NativeApiProtocolHostObject; +class NativeApiJsiArgumentFrame; + +Value callCFunction(Runtime& runtime, + const std::shared_ptr& bridge, + const NativeApiSymbol& symbol, const Value* args, + size_t count); + +Value callObjCSelector(Runtime& runtime, + const std::shared_ptr& bridge, + id receiver, bool receiverIsClass, + const std::string& selectorName, + const NativeApiMember* member, + const Value* args, size_t count, + Class dispatchSuperClass = Nil); + +Value makeNativeObjectValue(Runtime& runtime, + const std::shared_ptr& bridge, + id object, bool ownsObject); + +Value makeNativeClassValue(Runtime& runtime, + const std::shared_ptr& bridge, + NativeApiSymbol symbol); + +Object symbolToObject(Runtime& runtime, const NativeApiSymbol& symbol) { + Object result(runtime); + result.setProperty(runtime, "kind", makeString(runtime, kindName(symbol.kind))); + result.setProperty(runtime, "name", makeString(runtime, symbol.name)); + result.setProperty(runtime, "runtimeName", + makeString(runtime, symbol.runtimeName)); + result.setProperty(runtime, "metadataOffset", + static_cast(symbol.offset)); + + if (symbol.kind == NativeApiSymbolKind::Class) { + Class cls = objc_lookUpClass(symbol.runtimeName.c_str()); + result.setProperty(runtime, "available", cls != nil); + if (cls != nil) { + char address[32] = {}; + snprintf(address, sizeof(address), "%p", cls); + result.setProperty(runtime, "nativeAddress", makeString(runtime, address)); + } + } else if (symbol.kind == NativeApiSymbolKind::Struct || + symbol.kind == NativeApiSymbolKind::Union) { + result.setProperty(runtime, "available", true); + } + + return result; +} + +size_t nativeSizeForType(const NativeApiJsiType& type); +std::optional parseArrayIndexProperty(const std::string& property); + +NativeApiJsiType nativeObjectReturnType( + MDTypeKind kind = metagen::mdTypeAnyObject) { + NativeApiJsiType type; + type.kind = kind; + type.ffiType = &ffi_type_pointer; + type.supported = true; + return type; +} + +NativeApiJsiType nativeObjectReturnTypeForClass(Class cls) { + if (cls != Nil) { + const char* name = class_getName(cls); + if (name != nullptr && std::strcmp(name, "NSString") == 0) { + return nativeObjectReturnType(metagen::mdTypeNSStringObject); + } + if (name != nullptr && std::strcmp(name, "NSMutableString") == 0) { + return nativeObjectReturnType(metagen::mdTypeNSMutableStringObject); + } + } + return nativeObjectReturnType(metagen::mdTypeInstanceObject); +} + +Value convertNativeReturnValue(Runtime& runtime, + const std::shared_ptr& bridge, + const NativeApiJsiType& type, void* value); +Object createPointer(Runtime& runtime, + const std::shared_ptr& bridge, + void* pointer, bool adopted = false); + +NativeApiJsiType primitiveInteropType(MDTypeKind kind); diff --git a/NativeScript/ffi/shared/jsi/NativeApiJsiCallbacks.h b/NativeScript/ffi/shared/jsi/NativeApiJsiCallbacks.h new file mode 100644 index 00000000..2496bb5a --- /dev/null +++ b/NativeScript/ffi/shared/jsi/NativeApiJsiCallbacks.h @@ -0,0 +1,1502 @@ +bool isObjectiveCObjectType(const NativeApiJsiType& type) { + switch (type.kind) { + case metagen::mdTypeAnyObject: + case metagen::mdTypeProtocolObject: + case metagen::mdTypeClassObject: + case metagen::mdTypeInstanceObject: + case metagen::mdTypeNSStringObject: + case metagen::mdTypeNSMutableStringObject: + return true; + default: + return false; + } +} + +#ifndef NATIVESCRIPT_NATIVE_API_RETAIN_RUNTIME +std::shared_ptr retainNativeApiJsiRuntime(Runtime& runtime) { + return std::shared_ptr(&runtime, [](Runtime*) {}); +} +#endif + +#ifndef NATIVESCRIPT_NATIVE_API_RUNTIME_SCOPE +class NativeApiJsiRuntimeScope final { + public: + explicit NativeApiJsiRuntimeScope(Runtime&) {} +}; +#endif + +struct NativeApiJsiSignature { + ffi_cif cif = {}; + NativeApiJsiType returnType; + std::vector argumentTypes; + std::vector ffiTypes; + std::string selectorName; + bool variadic = false; + bool prepared = false; + unsigned int implicitArgumentCount = 0; +}; + +bool selectorEndsWithNSErrorParam(const std::string& selectorName) { + constexpr const char* suffix = "error:"; + size_t suffixLength = std::strlen(suffix); + return selectorName.size() >= suffixLength && + selectorName.compare(selectorName.size() - suffixLength, suffixLength, + suffix) == 0; +} + +bool isNSErrorOutJsiMethodSignature(const NativeApiJsiSignature& signature) { + if (signature.argumentTypes.empty() || signature.variadic || + !selectorEndsWithNSErrorParam(signature.selectorName)) { + return false; + } + + return signature.argumentTypes.back().kind == metagen::mdTypePointer; +} + +bool isNSErrorOutJsiMethodCallback(const NativeApiJsiSignature& signature) { + return signature.returnType.kind == metagen::mdTypeBool && + signature.implicitArgumentCount >= 2 && + isNSErrorOutJsiMethodSignature(signature); +} + +class NativeApiJsiArgumentFrame { + public: + explicit NativeApiJsiArgumentFrame(size_t count) : storage_(count), values_(count) {} + + ~NativeApiJsiArgumentFrame() { + for (char* string : ownedCStrings_) { + free(string); + } + for (void* buffer : ownedBuffers_) { + free(buffer); + } + for (id object : ownedObjects_) { + [object release]; + } + for (const auto& entry : temporaryRoundTripValues_) { + if (entry.first != nullptr) { + entry.first->forgetRoundTripValue(entry.second); + } + } + ownedLifetimes_.clear(); + } + + void* storageAt(size_t index, size_t size) { + storage_[index].assign(std::max(size, sizeof(void*)), 0); + values_[index] = storage_[index].data(); + return values_[index]; + } + + void addCString(char* value) { ownedCStrings_.push_back(value); } + void* addBuffer(size_t size) { + void* buffer = calloc(1, std::max(size, 1)); + if (buffer == nullptr) { + throw std::bad_alloc(); + } + ownedBuffers_.push_back(buffer); + return buffer; + } + void addObject(id value) { ownedObjects_.push_back(value); } + void addLifetime(std::shared_ptr value) { + if (value != nullptr) { + ownedLifetimes_.push_back(std::move(value)); + } + } + void rememberRoundTripValue( + const std::shared_ptr& bridge, Runtime& runtime, + const void* native, const Value& value) { + if (bridge == nullptr || native == nullptr) { + return; + } + bridge->rememberRoundTripValue(runtime, native, value); + temporaryRoundTripValues_.push_back({bridge, native}); + } + void** values() { return values_.empty() ? nullptr : values_.data(); } + + private: + std::vector> storage_; + std::vector values_; + std::vector ownedCStrings_; + std::vector ownedBuffers_; + std::vector ownedObjects_; + std::vector> ownedLifetimes_; + std::vector, const void*>> + temporaryRoundTripValues_; +}; + +class NativeApiMutableBuffer final : public MutableBuffer { + public: + explicit NativeApiMutableBuffer(size_t size) : data_(size) {} + NativeApiMutableBuffer(const void* data, size_t size) : data_(size) { + if (data != nullptr && size > 0) { + std::memcpy(data_.data(), data, size); + } + } + + size_t size() const override { return data_.size(); } + uint8_t* data() override { return data_.empty() ? nullptr : data_.data(); } + + private: + std::vector data_; +}; + +void convertJsiArgument(Runtime& runtime, + const std::shared_ptr& bridge, + const NativeApiJsiType& type, + const Value& value, void* target, + NativeApiJsiArgumentFrame& frame); + +Value convertNativeReturnValue(Runtime& runtime, + const std::shared_ptr& bridge, + const NativeApiJsiType& type, void* value); + +Value wrapNativeFunctionPointer(Runtime& runtime, + const std::shared_ptr& bridge, + const NativeApiJsiType& type, void* pointer, + bool block); + +bool isObjectiveCObjectType(const NativeApiJsiType& type); + +struct NativeApiJsiBlockDescriptor { + unsigned long reserved = 0; + unsigned long size = 0; + void (*copyHelper)(void*, void*) = nullptr; + void (*disposeHelper)(void*) = nullptr; + const char* signature = nullptr; +}; + +struct NativeApiJsiBlockLiteral { + void* isa = nullptr; + int flags = 0; + int reserved = 0; + void* invoke = nullptr; + NativeApiJsiBlockDescriptor* descriptor = nullptr; + void* callback = nullptr; +}; + +constexpr int kNativeApiJsiBlockNeedsFree = (1 << 24); +constexpr int kNativeApiJsiBlockHasCopyDispose = (1 << 25); +constexpr int kNativeApiJsiBlockRefCountOne = (1 << 1); +constexpr int kNativeApiJsiBlockHasSignature = (1 << 30); + +void* nativeApiJsiStackBlockIsa() { + static void* isa = dlsym(RTLD_DEFAULT, "_NSConcreteStackBlock"); + if (isa == nullptr) { + isa = dlsym(RTLD_DEFAULT, "_NSConcreteMallocBlock"); + } + return isa; +} + +void nativeApiJsiBlockCopy(void* dst, void* src); +void nativeApiJsiBlockDispose(void* src); + +std::string objcEncodingForJsiType(const NativeApiJsiType& type) { + switch (type.kind) { + case metagen::mdTypeVoid: + return "v"; + case metagen::mdTypeBool: + return "B"; + case metagen::mdTypeChar: + return "c"; + case metagen::mdTypeUChar: + case metagen::mdTypeUInt8: + return "C"; + case metagen::mdTypeSShort: + return "s"; + case metagen::mdTypeUShort: + return "S"; + case metagen::mdTypeSInt: + return "i"; + case metagen::mdTypeUInt: + return "I"; + case metagen::mdTypeSLong: + case metagen::mdTypeSInt64: + return "q"; + case metagen::mdTypeULong: + case metagen::mdTypeUInt64: + return "Q"; + case metagen::mdTypeFloat: + return "f"; + case metagen::mdTypeDouble: + return "d"; + case metagen::mdTypeString: + return "*"; + case metagen::mdTypeAnyObject: + case metagen::mdTypeProtocolObject: + case metagen::mdTypeInstanceObject: + case metagen::mdTypeNSStringObject: + case metagen::mdTypeNSMutableStringObject: + return "@"; + case metagen::mdTypeClassObject: + case metagen::mdTypeClass: + return "#"; + case metagen::mdTypeSelector: + return ":"; + case metagen::mdTypeBlock: + return "@?"; + case metagen::mdTypeFunctionPointer: + return "^?"; + case metagen::mdTypePointer: + case metagen::mdTypeOpaquePointer: + if (type.elementType != nullptr && + type.elementType->kind != metagen::mdTypeVoid) { + return "^" + objcEncodingForJsiType(*type.elementType); + } + return "^v"; + case metagen::mdTypeStruct: + return "{" + + (type.aggregateInfo != nullptr ? type.aggregateInfo->name + : std::string("?")) + + "=}"; + case metagen::mdTypeArray: + return "[" + std::to_string(type.arraySize) + + (type.elementType != nullptr ? objcEncodingForJsiType(*type.elementType) + : std::string("?")) + + "]"; + case metagen::mdTypeVector: + case metagen::mdTypeExtVector: + case metagen::mdTypeComplex: + return type.elementType != nullptr ? objcEncodingForJsiType(*type.elementType) + : "?"; + default: + return "?"; + } +} + +std::string objcBlockSignatureForJsiSignature( + const NativeApiJsiSignature& signature) { + std::string encoding = objcEncodingForJsiType(signature.returnType); + encoding += "@?"; + for (const auto& argType : signature.argumentTypes) { + encoding += objcEncodingForJsiType(argType); + } + return encoding; +} + +std::string objcMethodSignatureForJsiSignature( + const NativeApiJsiSignature& signature) { + std::string encoding = objcEncodingForJsiType(signature.returnType); + encoding += "@:"; + for (const auto& argType : signature.argumentTypes) { + encoding += objcEncodingForJsiType(argType); + } + return encoding; +} + +[[noreturn]] void throwNativeApiJsiCallbackException( + const std::string& message) { + NSString* reason = [NSString stringWithUTF8String:message.c_str()]; + @throw [NSException exceptionWithName:@"NativeScriptJSICallbackException" + reason:reason + userInfo:nil]; +} + +class NativeApiJsiCallback; + +void nativeApiJsiCallbackTrampoline(ffi_cif* cif, void* ret, void* args[], + void* data); + +std::atomic gActiveNativeThreadJsiCallbacks{0}; + +class NativeApiJsiCallback final + : public std::enable_shared_from_this { + public: + NativeApiJsiCallback(Runtime& runtime, + std::shared_ptr bridge, + std::shared_ptr signature, + Function function, bool block, bool bindThis = false) + : runtimeOwner_(retainNativeApiJsiRuntime(runtime)), + runtime_(runtimeOwner_.get()), + bridge_(std::move(bridge)), + signature_(std::move(signature)), + function_(std::make_shared(std::move(function))), + block_(block), + bindThis_(bindThis) { + closure_ = static_cast( + ffi_closure_alloc(sizeof(ffi_closure), &executable_)); + if (closure_ == nullptr || executable_ == nullptr || + signature_ == nullptr || !signature_->prepared) { + throw facebook::jsi::JSError(runtime, + "Unable to allocate native JSI callback."); + } + + ffi_status status = ffi_prep_closure_loc( + closure_, &signature_->cif, nativeApiJsiCallbackTrampoline, this, + executable_); + if (status != FFI_OK) { + ffi_closure_free(closure_); + closure_ = nullptr; + executable_ = nullptr; + throw facebook::jsi::JSError(runtime, + "Unable to prepare native JSI callback."); + } + + if (block_) { + blockSignature_ = objcBlockSignatureForJsiSignature(*signature_); + descriptor_ = std::make_unique(); + descriptor_->reserved = 0; + descriptor_->size = sizeof(NativeApiJsiBlockLiteral); + descriptor_->copyHelper = nativeApiJsiBlockCopy; + descriptor_->disposeHelper = nativeApiJsiBlockDispose; + descriptor_->signature = blockSignature_.c_str(); + + blockLiteral_ = std::make_unique(); + blockLiteral_->isa = nativeApiJsiStackBlockIsa(); + blockLiteral_->flags = kNativeApiJsiBlockHasCopyDispose | + kNativeApiJsiBlockHasSignature; + blockLiteral_->invoke = executable_; + blockLiteral_->descriptor = descriptor_.get(); + blockLiteral_->callback = this; + } + } + + ~NativeApiJsiCallback() { + if (closure_ != nullptr) { + ffi_closure_free(closure_); + closure_ = nullptr; + executable_ = nullptr; + } + } + + void* functionPointer() const { + return block_ && blockLiteral_ != nullptr + ? static_cast(blockLiteral_.get()) + : executable_; + } + + const NativeApiJsiSignature& signature() const { return *signature_; } + + void retainBlockCopy(const void* blockPointer) { + if (!block_) { + return; + } + auto self = shared_from_this(); + if (bridge_ != nullptr && runtime_ != nullptr && function_ != nullptr && + blockPointer != nullptr) { + bridge_->rememberRoundTripValue(*runtime_, blockPointer, + Value(*runtime_, *function_)); + } + std::lock_guard lock(retainedBlockCopiesMutex_); + retainedBlockCopies_.push_back({blockPointer, std::move(self)}); + } + + void releaseBlockCopy(const void* blockPointer) { + if (!block_) { + return; + } + std::shared_ptr keepAlive; + try { + keepAlive = shared_from_this(); + } catch (const std::bad_weak_ptr&) { + return; + } + std::lock_guard lock(retainedBlockCopiesMutex_); + auto it = retainedBlockCopies_.end(); + if (blockPointer != nullptr) { + it = std::find_if( + retainedBlockCopies_.begin(), retainedBlockCopies_.end(), + [blockPointer](const RetainedBlockCopy& retained) { + return retained.blockPointer == blockPointer; + }); + } + if (it == retainedBlockCopies_.end() && !retainedBlockCopies_.empty()) { + it = retainedBlockCopies_.end() - 1; + } + if (it != retainedBlockCopies_.end()) { + if (bridge_ != nullptr && it->blockPointer != nullptr) { + bridge_->forgetRoundTripValue(it->blockPointer); + } + retainedBlockCopies_.erase(it); + } + } + + void invoke(void* ret, void* args[]) { + if (runtime_ == nullptr || function_ == nullptr || signature_ == nullptr) { + throwNativeApiJsiCallbackException("Invalid JSI callback."); + } + + std::string error; + auto call = [&]() { invokeOnCurrentThread(ret, args, &error); }; + const auto& nativeCallbackInvoker = bridge_->nativeCallbackInvoker(); + const auto& jsThreadCallbackInvoker = bridge_->jsThreadCallbackInvoker(); + bool currentThreadIsJs = + std::this_thread::get_id() == bridge_->jsThreadId(); + bool returnsVoid = signature_->returnType.kind == metagen::mdTypeVoid; + bool activeSynchronousNativeInvocation = + gActiveSynchronousNativeInvocationDepth.load( + std::memory_order_acquire) > 0; + bool nativeCallerThreadCallback = + !currentThreadIsJs && activeSynchronousNativeInvocation && + (!block_ || !returnsVoid); + bool direct = currentThreadIsJs || + gExecutingDispatchedUINativeCall || + gSynchronousNativeInvocationDepth > 0 || + nativeCallerThreadCallback || + (!nativeCallbackInvoker && + activeSynchronousNativeInvocation); + bool waitForNativeThreadCallback = + currentThreadIsJs && nativeCallbackInvoker && + gActiveNativeThreadJsiCallbacks.load(std::memory_order_acquire) > 0; + if (direct && !waitForNativeThreadCallback) { + if (nativeCallerThreadCallback) { + ScopedNativeCallerThreadJsiCallback callbackScope; + call(); + } else { + call(); + } + } else if (!currentThreadIsJs && returnsVoid && block_ && + jsThreadCallbackInvoker) { + jsThreadCallbackInvoker(call); + } else if (nativeCallbackInvoker) { + bool nativeThreadCallback = !currentThreadIsJs; + if (nativeThreadCallback) { + gActiveNativeThreadJsiCallbacks.fetch_add(1, + std::memory_order_acq_rel); + } + try { + nativeCallbackInvoker(call); + } catch (...) { + if (nativeThreadCallback) { + gActiveNativeThreadJsiCallbacks.fetch_sub( + 1, std::memory_order_acq_rel); + } + throw; + } + if (nativeThreadCallback) { + gActiveNativeThreadJsiCallbacks.fetch_sub(1, + std::memory_order_acq_rel); + } + } else if (auto scheduler = bridge_->scheduler()) { + dispatch_semaphore_t done = dispatch_semaphore_create(0); + scheduler->invokeOnJS([call, done]() mutable { + call(); + dispatch_semaphore_signal(done); + }); + dispatch_semaphore_wait(done, DISPATCH_TIME_FOREVER); + } else { + error = "Native callback was invoked off the JS thread without a JS scheduler."; + } + + if (!error.empty()) { + throwNativeApiJsiCallbackException(error); + } + } + + private: + void invokeOnCurrentThread(void* ret, void* args[], std::string* error) { + try { + NativeApiJsiRuntimeScope runtimeScope(*runtime_); + size_t nativeArgOffset = signature_->implicitArgumentCount; + std::vector jsArgs; + jsArgs.reserve(signature_->argumentTypes.size()); + for (size_t i = 0; i < signature_->argumentTypes.size(); i++) { + jsArgs.emplace_back(convertNativeReturnValue( + *runtime_, bridge_, signature_->argumentTypes[i], + args[i + nativeArgOffset])); + } + + Value result = Value::undefined(); + if (bindThis_ && nativeArgOffset >= 1) { + id self = *static_cast(args[0]); + Value thisValue = + makeNativeObjectValue(*runtime_, bridge_, self, false); + Object thisObject = thisValue.isObject() + ? thisValue.asObject(*runtime_) + : Object(*runtime_); + result = + jsArgs.empty() + ? function_->callWithThis(*runtime_, thisObject) + : function_->callWithThis( + *runtime_, thisObject, + static_cast(jsArgs.data()), + static_cast(jsArgs.size())); + } else { + result = + jsArgs.empty() + ? function_->call(*runtime_) + : function_->call(*runtime_, + static_cast(jsArgs.data()), + static_cast(jsArgs.size())); + } + storeReturnValue(result, ret); + if (std::this_thread::get_id() == bridge_->jsThreadId()) { + runtime_->drainMicrotasks(); + } + } catch (const std::exception& exception) { + if (isNSErrorOutJsiMethodCallback(*signature_)) { + zeroReturnValue(ret); + populateNSErrorOutArgument(args, exception.what()); + return; + } + if (error != nullptr) { + *error = exception.what(); + } + zeroReturnValue(ret); + } catch (...) { + if (isNSErrorOutJsiMethodCallback(*signature_)) { + zeroReturnValue(ret); + populateNSErrorOutArgument(args, "Unknown exception in native JSI callback."); + return; + } + if (error != nullptr) { + *error = "Unknown exception in native JSI callback."; + } + zeroReturnValue(ret); + } + } + + void populateNSErrorOutArgument(void* args[], const char* message) { + if (args == nullptr || signature_ == nullptr || + signature_->argumentTypes.empty()) { + return; + } + + size_t outArgIndex = signature_->implicitArgumentCount + + signature_->argumentTypes.size() - 1; + void* outArgValue = args[outArgIndex]; + NSError** outError = + outArgValue != nullptr ? *reinterpret_cast(outArgValue) + : nullptr; + if (outError == nullptr) { + return; + } + + NSString* nsMessage = + message != nullptr ? [NSString stringWithUTF8String:message] : nil; + if (nsMessage == nil) { + nsMessage = @"JS error"; + } + NSDictionary* userInfo = @{NSLocalizedDescriptionKey : nsMessage}; + *outError = [NSError errorWithDomain:@"TNSErrorDomain" + code:1 + userInfo:userInfo]; + } + + void zeroReturnValue(void* ret) { + if (ret == nullptr || signature_ == nullptr || + signature_->returnType.kind == metagen::mdTypeVoid) { + return; + } + size_t size = nativeSizeForType(signature_->returnType); + if (size > 0) { + std::memset(ret, 0, size); + } + } + + void storeReturnValue(const Value& result, void* ret) { + if (ret == nullptr || + signature_->returnType.kind == metagen::mdTypeVoid) { + return; + } + + zeroReturnValue(ret); + if (result.isUndefined() || result.isNull()) { + return; + } + const auto& returnType = signature_->returnType; + if (returnType.kind == metagen::mdTypeString && result.isString()) { + std::string utf8 = result.asString(*runtime_).utf8(*runtime_); + *static_cast(ret) = strdup(utf8.c_str()); + return; + } + if ((returnType.kind == metagen::mdTypePointer || + returnType.kind == metagen::mdTypeOpaquePointer) && + result.isString()) { + std::string utf8 = result.asString(*runtime_).utf8(*runtime_); + *static_cast(ret) = strdup(utf8.c_str()); + return; + } + + NativeApiJsiArgumentFrame frame(1); + convertJsiArgument(*runtime_, bridge_, returnType, result, ret, frame); + if (isObjectiveCObjectType(returnType)) { + id object = *static_cast(ret); + if (object != nil) { + [object retain]; + [object autorelease]; + } + } + } + + std::shared_ptr runtimeOwner_; + Runtime* runtime_ = nullptr; + std::shared_ptr bridge_; + std::shared_ptr signature_; + std::shared_ptr function_; + bool block_ = false; + bool bindThis_ = false; + ffi_closure* closure_ = nullptr; + void* executable_ = nullptr; + std::string blockSignature_; + std::unique_ptr descriptor_; + std::unique_ptr blockLiteral_; + struct RetainedBlockCopy { + const void* blockPointer = nullptr; + std::shared_ptr lifetime; + }; + std::mutex retainedBlockCopiesMutex_; + std::vector retainedBlockCopies_; +}; + +void nativeApiJsiBlockCopy(void* dst, void* src) { + auto* dstBlock = static_cast(dst); + auto* srcBlock = static_cast(src); + if (dstBlock == nullptr || srcBlock == nullptr || + srcBlock->callback == nullptr) { + return; + } + dstBlock->callback = srcBlock->callback; + static_cast(srcBlock->callback) + ->retainBlockCopy(dstBlock); +} + +void nativeApiJsiBlockDispose(void* src) { + auto* block = static_cast(src); + if (block == nullptr || block->callback == nullptr) { + return; + } + static_cast(block->callback)->releaseBlockCopy(block); + block->callback = nullptr; +} + +void nativeApiJsiCallbackTrampoline(ffi_cif*, void* ret, void* args[], + void* data) { + auto callback = static_cast(data); + if (callback == nullptr) { + return; + } + callback->invoke(ret, args); +} + +size_t nativeSizeForType(const NativeApiJsiType& type) { + switch (type.kind) { + case metagen::mdTypeStruct: + if (type.aggregateInfo != nullptr) { + return type.aggregateInfo->size; + } + break; + case metagen::mdTypeArray: + if (type.elementType != nullptr) { + return nativeSizeForType(*type.elementType) * + static_cast(type.arraySize); + } + break; + case metagen::mdTypeVector: + case metagen::mdTypeExtVector: + case metagen::mdTypeComplex: + if (type.elementType != nullptr) { + size_t lanes = std::max(type.arraySize, 1); + size_t abiLanes = lanes == 3 ? 4 : lanes; + return nativeSizeForType(*type.elementType) * abiLanes; + } + break; + default: + break; + } + + if (type.ffiType != nullptr && type.ffiType->size > 0) { + return type.ffiType->size; + } + if (type.ffiType == &ffi_type_void) { + return 0; + } + return sizeof(void*); +} + +Value signedInteger64ToJsiValue(Runtime& runtime, int64_t value) { + constexpr int64_t maxSafeInteger = 9007199254740991LL; + constexpr int64_t minSafeInteger = -9007199254740991LL; + if (value >= minSafeInteger && value <= maxSafeInteger) { + return static_cast(value); + } + return BigInt::fromInt64(runtime, value); +} + +Value unsignedInteger64ToJsiValue(Runtime& runtime, uint64_t value) { + constexpr uint64_t maxSafeInteger = 9007199254740991ULL; + if (value <= maxSafeInteger) { + return static_cast(value); + } + return BigInt::fromUint64(runtime, value); +} + +bool parseIntegerTextToUintptr(const std::string& text, uintptr_t* address) { + if (address == nullptr) { + return false; + } + if (text.empty()) { + return false; + } + + char* end = nullptr; + if (text[0] == '-') { + long long signedValue = std::strtoll(text.c_str(), &end, 10); + if (end == nullptr || *end != '\0') { + return false; + } + *address = static_cast(static_cast(signedValue)); + return true; + } + + int base = 10; + const char* start = text.c_str(); + if (text.size() > 2 && text[0] == '0' && + (text[1] == 'x' || text[1] == 'X')) { + base = 16; + } + unsigned long long unsignedValue = std::strtoull(start, &end, base); + if (end == nullptr || *end != '\0') { + return false; + } + *address = static_cast(unsignedValue); + return true; +} + +bool parseBigIntToUintptr(Runtime& runtime, const BigInt& bigint, + uintptr_t* address) { + return parseIntegerTextToUintptr(bigint.toString(runtime, 10).utf8(runtime), + address); +} + +bool readJsiBuffer(Runtime& runtime, const Object& object, const uint8_t** data, + size_t* byteLength) { + if (data == nullptr || byteLength == nullptr) { + return false; + } + + if (object.isArrayBuffer(runtime)) { + ArrayBuffer buffer = object.getArrayBuffer(runtime); + *data = buffer.data(runtime); + *byteLength = buffer.size(runtime); + return true; + } + + Value bufferValue = object.getProperty(runtime, "buffer"); + if (!bufferValue.isObject()) { + return false; + } + Object bufferObject = bufferValue.asObject(runtime); + if (!bufferObject.isArrayBuffer(runtime)) { + return false; + } + + size_t byteOffset = 0; + size_t viewByteLength = 0; + Value offsetValue = object.getProperty(runtime, "byteOffset"); + if (offsetValue.isNumber()) { + byteOffset = static_cast(std::max(0, offsetValue.getNumber())); + } + Value lengthValue = object.getProperty(runtime, "byteLength"); + if (lengthValue.isNumber()) { + viewByteLength = static_cast(std::max(0, lengthValue.getNumber())); + } + + ArrayBuffer buffer = bufferObject.getArrayBuffer(runtime); + if (byteOffset > buffer.size(runtime)) { + return false; + } + if (viewByteLength == 0 || byteOffset + viewByteLength > buffer.size(runtime)) { + viewByteLength = buffer.size(runtime) - byteOffset; + } + *data = buffer.data(runtime) + byteOffset; + *byteLength = viewByteLength; + return true; +} + +uint32_t rawTypeKind(MDTypeKind kind) { + return static_cast(kind); +} + +MDTypeKind stripTypeFlags(MDTypeKind kind) { + uint32_t raw = rawTypeKind(kind); + raw &= ~static_cast(metagen::mdTypeFlagNext); + raw &= ~static_cast(metagen::mdTypeFlagVariadic); + return static_cast(raw); +} + +size_t alignUp(size_t value, size_t alignment) { + if (alignment == 0) { + return value; + } + return ((value + alignment - 1) / alignment) * alignment; +} + +ffi_type* ffiTypeForJsiKind(MDTypeKind kind) { + switch (kind) { + case metagen::mdTypeChar: + return &ffi_type_sint8; + case metagen::mdTypeUChar: + case metagen::mdTypeUInt8: + case metagen::mdTypeBool: + return &ffi_type_uint8; + case metagen::mdTypeSShort: + return &ffi_type_sint16; + case metagen::mdTypeUShort: + return &ffi_type_uint16; + case metagen::mdTypeSInt: + return &ffi_type_sint32; + case metagen::mdTypeUInt: + return &ffi_type_uint32; + case metagen::mdTypeSLong: + case metagen::mdTypeSInt64: + return &ffi_type_sint64; + case metagen::mdTypeULong: + case metagen::mdTypeUInt64: + return &ffi_type_uint64; + case metagen::mdTypeFloat: + return &ffi_type_float; + case metagen::mdTypeDouble: + return &ffi_type_double; + case metagen::mdTypeVoid: + return &ffi_type_void; + case metagen::mdTypeString: + case metagen::mdTypeAnyObject: + case metagen::mdTypeProtocolObject: + case metagen::mdTypeClassObject: + case metagen::mdTypeInstanceObject: + case metagen::mdTypeNSStringObject: + case metagen::mdTypeNSMutableStringObject: + case metagen::mdTypeClass: + case metagen::mdTypeSelector: + case metagen::mdTypePointer: + case metagen::mdTypeOpaquePointer: + case metagen::mdTypeBlock: + case metagen::mdTypeFunctionPointer: + return &ffi_type_pointer; + default: + return nullptr; + } +} + +bool isSupportedJsiKind(MDTypeKind kind) { + switch (kind) { + default: + return ffiTypeForJsiKind(kind) != nullptr; + } +} + +void skipMetadataJsiTypePayload(MDMetadataReader* metadata, MDSectionOffset* offset, + MDTypeKind kind); + +void skipMetadataJsiType(MDMetadataReader* metadata, MDSectionOffset* offset) { + MDTypeKind kind = stripTypeFlags(metadata->getTypeKind(*offset)); + *offset += sizeof(MDTypeKind); + skipMetadataJsiTypePayload(metadata, offset, kind); +} + +void skipMetadataJsiTypePayload(MDMetadataReader* metadata, MDSectionOffset* offset, + MDTypeKind kind) { + switch (kind) { + case metagen::mdTypeClassObject: { + auto classOffset = metadata->getOffset(*offset); + *offset += sizeof(MDSectionOffset); + bool next = (classOffset & metagen::mdSectionOffsetNext) != 0; + while (next) { + auto protocolOffset = metadata->getOffset(*offset); + *offset += sizeof(MDSectionOffset); + next = (protocolOffset & metagen::mdSectionOffsetNext) != 0; + } + break; + } + case metagen::mdTypeProtocolObject: { + bool next = true; + while (next) { + auto protocolOffset = metadata->getOffset(*offset); + *offset += sizeof(MDSectionOffset); + next = (protocolOffset & metagen::mdSectionOffsetNext) != 0; + } + break; + } + case metagen::mdTypeArray: + case metagen::mdTypeVector: + case metagen::mdTypeExtVector: + case metagen::mdTypeComplex: + *offset += sizeof(uint16_t); + skipMetadataJsiType(metadata, offset); + break; + case metagen::mdTypeStruct: + *offset += sizeof(MDSectionOffset); + break; + case metagen::mdTypePointer: + skipMetadataJsiType(metadata, offset); + break; + case metagen::mdTypeBlock: + case metagen::mdTypeFunctionPointer: + *offset += sizeof(MDSectionOffset); + break; + default: + break; + } +} + +NativeApiJsiType parseMetadataJsiType(MDMetadataReader* metadata, + MDSectionOffset* offset, + NativeApiJsiBridge* bridge) { + MDTypeKind rawKind = metadata->getTypeKind(*offset); + MDTypeKind kind = stripTypeFlags(rawKind); + *offset += sizeof(MDTypeKind); + + NativeApiJsiType type; + type.kind = kind; + + switch (kind) { + case metagen::mdTypeArray: { + type.arraySize = metadata->getArraySize(*offset); + *offset += sizeof(uint16_t); + type.elementType = + std::make_shared( + parseMetadataJsiType(metadata, offset, bridge)); + auto ffiOwner = std::make_shared(); + ffiOwner->elements.reserve(static_cast(type.arraySize) + 1); + ffi_type* elementFfiType = type.elementType->ffiType != nullptr + ? type.elementType->ffiType + : &ffi_type_pointer; + for (uint16_t i = 0; i < type.arraySize; i++) { + ffiOwner->elements.push_back(elementFfiType); + } + ffiOwner->finalize(); + type.ownedFfiType = ffiOwner; + type.ffiType = &ffiOwner->type; + type.supported = type.elementType->supported; + return type; + } + case metagen::mdTypeVector: + case metagen::mdTypeExtVector: + case metagen::mdTypeComplex: { + type.arraySize = metadata->getArraySize(*offset); + *offset += sizeof(uint16_t); + type.elementType = + std::make_shared( + parseMetadataJsiType(metadata, offset, bridge)); + auto ffiOwner = std::make_shared(); +#if defined(FFI_TYPE_EXT_VECTOR) + ffiOwner->type.type = + kind == metagen::mdTypeComplex ? FFI_TYPE_COMPLEX : FFI_TYPE_EXT_VECTOR; +#else + ffiOwner->type.type = + kind == metagen::mdTypeComplex ? FFI_TYPE_COMPLEX : FFI_TYPE_STRUCT; +#endif + ffi_type* elementFfiType = type.elementType->ffiType != nullptr + ? type.elementType->ffiType + : &ffi_type_float; + size_t lanes = std::max(type.arraySize, 1); + size_t abiLanes = lanes == 3 ? 4 : lanes; + size_t elementSize = std::max(elementFfiType->size, sizeof(float)); + size_t elementAlignment = + std::max(elementFfiType->alignment, static_cast(1)); + ffiOwner->elements.reserve(abiLanes + 1); + for (size_t i = 0; i < abiLanes; i++) { + ffiOwner->elements.push_back(elementFfiType); + } + ffiOwner->finalize(); + size_t vectorAlignment = elementAlignment; + if (kind != metagen::mdTypeComplex) { + size_t packedSize = abiLanes * elementSize; + size_t preferredAlignment = packedSize >= 16 ? 16 : packedSize; + vectorAlignment = std::max(vectorAlignment, preferredAlignment); + } + vectorAlignment = std::min(vectorAlignment, 16); + ffiOwner->type.alignment = static_cast(vectorAlignment); + ffiOwner->type.size = alignUp(abiLanes * elementSize, vectorAlignment); + type.ownedFfiType = ffiOwner; + type.ffiType = &ffiOwner->type; + type.supported = type.elementType->supported; + return type; + } + case metagen::mdTypeStruct: { + auto structOffset = metadata->getOffset(*offset); + *offset += sizeof(MDSectionOffset); + bool isUnion = (structOffset & metagen::mdSectionOffsetNext) != 0; + structOffset &= ~metagen::mdSectionOffsetNext; + if (structOffset == MD_SECTION_OFFSET_NULL || bridge == nullptr) { + type.kind = metagen::mdTypePointer; + type.ffiType = &ffi_type_pointer; + type.supported = true; + return type; + } + + MDSectionOffset absoluteOffset = + structOffset + (isUnion ? metadata->unionsOffset : metadata->structsOffset); + type.aggregateOffset = absoluteOffset; + type.aggregateIsUnion = isUnion; + type.aggregateInfo = bridge->aggregateInfoFor(absoluteOffset, isUnion); + type.ffiType = type.aggregateInfo != nullptr && type.aggregateInfo->ffi != nullptr + ? &type.aggregateInfo->ffi->type + : nullptr; + type.supported = type.ffiType != nullptr; + return type; + } + case metagen::mdTypePointer: + type.elementType = + std::make_shared( + parseMetadataJsiType(metadata, offset, bridge)); + type.ffiType = &ffi_type_pointer; + type.supported = true; + return type; + case metagen::mdTypeBlock: + case metagen::mdTypeFunctionPointer: + type.signatureOffset = metadata->getOffset(*offset) + metadata->signaturesOffset; + *offset += sizeof(MDSectionOffset); + type.ffiType = &ffi_type_pointer; + type.supported = true; + return type; + case metagen::mdTypeClassObject: { + auto classOffset = metadata->getOffset(*offset); + *offset += sizeof(MDSectionOffset); + bool next = (classOffset & metagen::mdSectionOffsetNext) != 0; + while (next) { + auto protocolOffset = metadata->getOffset(*offset); + *offset += sizeof(MDSectionOffset); + next = (protocolOffset & metagen::mdSectionOffsetNext) != 0; + } + break; + } + case metagen::mdTypeProtocolObject: { + bool next = true; + while (next) { + auto protocolOffset = metadata->getOffset(*offset); + *offset += sizeof(MDSectionOffset); + next = (protocolOffset & metagen::mdSectionOffsetNext) != 0; + } + break; + } + default: + break; + } + + type.ffiType = ffiTypeForJsiKind(kind); + type.supported = type.ffiType != nullptr && isSupportedJsiKind(kind); + return type; +} + +std::shared_ptr NativeApiJsiBridge::aggregateInfoFor( + MDSectionOffset aggregateOffset, bool isUnion) { + if (metadata_ == nullptr || aggregateOffset == MD_SECTION_OFFSET_NULL) { + return nullptr; + } + + auto cached = aggregateInfoByOffset_.find(aggregateOffset); + if (cached != aggregateInfoByOffset_.end()) { + return cached->second; + } + + auto info = std::make_shared(); + info->offset = aggregateOffset; + info->isUnion = isUnion; + aggregateInfoByOffset_[aggregateOffset] = info; + + if (aggregateInfoInProgress_.find(aggregateOffset) != + aggregateInfoInProgress_.end()) { + auto ffiOwner = std::make_shared(); + ffiOwner->elements.push_back(&ffi_type_pointer); + ffiOwner->finalize(); + info->ffi = ffiOwner; + return info; + } + + aggregateInfoInProgress_.insert(aggregateOffset); + + MDSectionOffset offset = aggregateOffset; + const char* name = metadata_->getString(offset); + info->name = name != nullptr ? name : ""; + offset += sizeof(MDSectionOffset); + info->size = metadata_->getArraySize(offset); + offset += sizeof(uint16_t); + + bool next = true; + while (next) { + MDSectionOffset nameOffset = metadata_->getOffset(offset); + offset += sizeof(MDSectionOffset); + next = (nameOffset & metagen::mdSectionOffsetNext) != 0; + nameOffset &= ~metagen::mdSectionOffsetNext; + if (nameOffset == MD_SECTION_OFFSET_NULL) { + break; + } + + NativeApiJsiAggregateField field; + const char* fieldName = metadata_->resolveString(nameOffset); + field.name = fieldName != nullptr ? fieldName : ""; + if (!isUnion) { + field.offset = metadata_->getArraySize(offset); + offset += sizeof(uint16_t); + } + field.type = parseMetadataJsiType(metadata_.get(), &offset, this); + info->fields.push_back(std::move(field)); + } + + auto ffiOwner = std::make_shared(); + if (isUnion) { + ffi_type* largest = &ffi_type_uint8; + size_t largestSize = 0; + for (const auto& field : info->fields) { + size_t fieldSize = nativeSizeForType(field.type); + if (field.type.ffiType != nullptr && fieldSize >= largestSize) { + largest = field.type.ffiType; + largestSize = fieldSize; + } + } + ffiOwner->elements.push_back(largest); + } else { + for (const auto& field : info->fields) { + ffiOwner->elements.push_back(field.type.ffiType != nullptr + ? field.type.ffiType + : &ffi_type_pointer); + } + if (ffiOwner->elements.empty()) { + ffiOwner->elements.push_back(&ffi_type_uint8); + } + } + ffiOwner->finalize(); + info->ffi = ffiOwner; + aggregateInfoInProgress_.erase(aggregateOffset); + return info; +} + +ffi_type* ffiTypeForJsiArgument(const NativeApiJsiType& type) { + switch (type.kind) { + case metagen::mdTypeArray: + return &ffi_type_pointer; + default: + return type.ffiType != nullptr ? type.ffiType : &ffi_type_pointer; + } +} + +std::optional parseMetadataJsiSignature( + MDMetadataReader* metadata, MDSectionOffset signatureOffset, + unsigned int implicitArgumentCount, NativeApiJsiBridge* bridge, + bool returnOwned = false) { + if (metadata == nullptr || signatureOffset == MD_SECTION_OFFSET_NULL) { + return std::nullopt; + } + + NativeApiJsiSignature signature; + signature.implicitArgumentCount = implicitArgumentCount; + + MDSectionOffset offset = signatureOffset; + MDTypeKind returnKind = metadata->getTypeKind(offset); + uint32_t returnKindRaw = rawTypeKind(returnKind); + bool next = + (returnKindRaw & static_cast(metagen::mdTypeFlagNext)) != 0; + signature.variadic = + (returnKindRaw & static_cast(metagen::mdTypeFlagVariadic)) != 0; + signature.returnType = parseMetadataJsiType(metadata, &offset, bridge); + signature.returnType.returnOwned = returnOwned; + + while (next) { + MDTypeKind argKind = metadata->getTypeKind(offset); + next = (rawTypeKind(argKind) & + static_cast(metagen::mdTypeFlagNext)) != 0; + signature.argumentTypes.push_back(parseMetadataJsiType(metadata, &offset, bridge)); + } + + signature.ffiTypes.reserve(signature.argumentTypes.size() + + implicitArgumentCount); + for (unsigned int i = 0; i < implicitArgumentCount; i++) { + signature.ffiTypes.push_back(&ffi_type_pointer); + } + for (const auto& argType : signature.argumentTypes) { + signature.ffiTypes.push_back(ffiTypeForJsiArgument(argType)); + } + + ffi_status status = ffi_prep_cif( + &signature.cif, FFI_DEFAULT_ABI, + static_cast(signature.ffiTypes.size()), + signature.returnType.ffiType != nullptr ? signature.returnType.ffiType + : &ffi_type_void, + signature.ffiTypes.empty() ? nullptr : signature.ffiTypes.data()); + signature.prepared = status == FFI_OK; + return signature; +} + +const char* skipObjCTypeQualifiers(const char* encoding) { + while (encoding != nullptr && *encoding != '\0' && + std::strchr("rnNoORV", *encoding) != nullptr) { + encoding++; + } + return encoding; +} + +NativeApiJsiType parseObjCEncodedJsiType( + const char* encoding, NativeApiJsiBridge* bridge = nullptr) { + encoding = skipObjCTypeQualifiers(encoding); + NativeApiJsiType type; + + if (encoding == nullptr || *encoding == '\0') { + type.kind = metagen::mdTypePointer; + type.ffiType = &ffi_type_pointer; + return type; + } + + switch (*encoding) { + case 'c': + type.kind = metagen::mdTypeChar; + break; + case 'i': + type.kind = metagen::mdTypeSInt; + break; + case 's': + type.kind = metagen::mdTypeSShort; + break; + case 'l': + case 'q': + type.kind = metagen::mdTypeSInt64; + break; + case 'C': + type.kind = metagen::mdTypeUInt8; + break; + case 'I': + type.kind = metagen::mdTypeUInt; + break; + case 'S': + type.kind = metagen::mdTypeUShort; + break; + case 'L': + case 'Q': + type.kind = metagen::mdTypeUInt64; + break; + case 'f': + type.kind = metagen::mdTypeFloat; + break; + case 'd': + type.kind = metagen::mdTypeDouble; + break; + case 'B': + type.kind = metagen::mdTypeBool; + break; + case 'v': + type.kind = metagen::mdTypeVoid; + break; + case '*': + type.kind = metagen::mdTypeString; + break; + case '@': + if (std::strncmp(encoding, "@\"NSString\"", 11) == 0) { + type.kind = metagen::mdTypeNSStringObject; + } else if (std::strncmp(encoding, "@\"NSMutableString\"", 18) == 0) { + type.kind = metagen::mdTypeNSMutableStringObject; + } else { + type.kind = metagen::mdTypeAnyObject; + } + break; + case '#': + type.kind = metagen::mdTypeClass; + break; + case ':': + type.kind = metagen::mdTypeSelector; + break; + case '^': + type.kind = metagen::mdTypePointer; + type.elementType = std::make_shared( + parseObjCEncodedJsiType(encoding + 1, bridge)); + type.ffiType = &ffi_type_pointer; + type.supported = true; + return type; + case '{': + case '(': { + type.kind = metagen::mdTypeStruct; + const char* nameStart = encoding + 1; + const char* nameEnd = nameStart; + while (*nameEnd != '\0' && *nameEnd != '=' && *nameEnd != '}' + && *nameEnd != ')') { + nameEnd++; + } + if (bridge != nullptr && nameEnd > nameStart) { + std::string aggregateName(nameStart, + static_cast(nameEnd - nameStart)); + const NativeApiSymbol* symbol = + *encoding == '(' ? bridge->findUnion(aggregateName) + : bridge->findStruct(aggregateName); + if (symbol == nullptr) { + symbol = bridge->findAggregate(aggregateName); + } + if (symbol != nullptr) { + type.aggregateOffset = symbol->offset; + type.aggregateIsUnion = symbol->kind == NativeApiSymbolKind::Union; + type.aggregateInfo = bridge->aggregateInfoFor(*symbol); + type.ffiType = + type.aggregateInfo != nullptr && type.aggregateInfo->ffi != nullptr + ? &type.aggregateInfo->ffi->type + : nullptr; + type.supported = type.ffiType != nullptr; + return type; + } + } + type.supported = false; + type.ffiType = nullptr; + return type; + } + case '[': + type.kind = metagen::mdTypeStruct; + type.supported = false; + type.ffiType = nullptr; + return type; + default: + type.kind = metagen::mdTypePointer; + break; + } + + type.ffiType = ffiTypeForJsiKind(type.kind); + type.supported = type.ffiType != nullptr; + return type; +} + +std::optional parseObjCMethodJsiSignature( + Method method, NativeApiJsiBridge* bridge = nullptr) { + if (method == nullptr) { + return std::nullopt; + } + + NativeApiJsiSignature signature; + signature.implicitArgumentCount = 2; + + char* returnEncoding = method_copyReturnType(method); + signature.returnType = parseObjCEncodedJsiType(returnEncoding, bridge); + if (returnEncoding != nullptr) { + free(returnEncoding); + } + + unsigned int totalArgc = method_getNumberOfArguments(method); + for (unsigned int i = 2; i < totalArgc; i++) { + char* argEncoding = method_copyArgumentType(method, i); + signature.argumentTypes.push_back(parseObjCEncodedJsiType(argEncoding, bridge)); + if (argEncoding != nullptr) { + free(argEncoding); + } + } + + signature.ffiTypes.reserve(totalArgc); + signature.ffiTypes.push_back(&ffi_type_pointer); + signature.ffiTypes.push_back(&ffi_type_pointer); + for (const auto& argType : signature.argumentTypes) { + signature.ffiTypes.push_back(ffiTypeForJsiArgument(argType)); + } + + ffi_status status = ffi_prep_cif( + &signature.cif, FFI_DEFAULT_ABI, + static_cast(signature.ffiTypes.size()), + signature.returnType.ffiType != nullptr ? signature.returnType.ffiType + : &ffi_type_void, + signature.ffiTypes.data()); + signature.prepared = status == FFI_OK; + return signature; +} + +bool prepareJsiMethodSignature(NativeApiJsiSignature* signature) { + if (signature == nullptr) { + return false; + } + signature->implicitArgumentCount = 2; + signature->ffiTypes.clear(); + signature->ffiTypes.reserve(signature->argumentTypes.size() + 2); + signature->ffiTypes.push_back(&ffi_type_pointer); + signature->ffiTypes.push_back(&ffi_type_pointer); + for (const auto& argType : signature->argumentTypes) { + ffi_type* ffiType = ffiTypeForJsiArgument(argType); + if (ffiType == nullptr) { + signature->prepared = false; + return false; + } + signature->ffiTypes.push_back(ffiType); + } + ffi_type* returnFfiType = + signature->returnType.ffiType != nullptr ? signature->returnType.ffiType + : &ffi_type_void; + signature->prepared = + ffi_prep_cif(&signature->cif, FFI_DEFAULT_ABI, + static_cast(signature->ffiTypes.size()), + returnFfiType, signature->ffiTypes.data()) == FFI_OK; + return signature->prepared; +} + +bool unsupportedJsiType(const NativeApiJsiType& type) { + if (type.kind == metagen::mdTypeStruct && type.aggregateInfo != nullptr && + type.aggregateInfo->ffi != nullptr) { + return false; + } + return !type.supported || type.ffiType == nullptr; +} + +bool signatureSupportedForJsiCallback(const NativeApiJsiSignature& signature) { + if (!signature.prepared || signature.variadic || + unsupportedJsiType(signature.returnType)) { + return false; + } + for (const auto& argType : signature.argumentTypes) { + if (unsupportedJsiType(argType)) { + return false; + } + } + return true; +} + +std::shared_ptr createJsiCallback( + Runtime& runtime, const std::shared_ptr& bridge, + const NativeApiJsiType& type, Function function, bool block) { + if (bridge == nullptr || bridge->metadata() == nullptr || + type.signatureOffset == MD_SECTION_OFFSET_NULL) { + throw facebook::jsi::JSError( + runtime, "Native callback metadata is unavailable."); + } + + auto parsed = parseMetadataJsiSignature( + bridge->metadata(), type.signatureOffset, block ? 1 : 0, bridge.get()); + if (!parsed || !signatureSupportedForJsiCallback(*parsed)) { + throw facebook::jsi::JSError( + runtime, "Native callback signature is not supported by pure JSI."); + } + + auto signature = + std::make_shared(std::move(*parsed)); + auto callback = std::make_shared( + runtime, bridge, std::move(signature), std::move(function), block); + if (!block) { + bridge->retainJsiLifetime(callback); + } + return callback; +} + +std::shared_ptr createJsiMethodCallback( + Runtime& runtime, const std::shared_ptr& bridge, + const std::string& selectorName, MDSectionOffset signatureOffset, + Function function, bool returnOwned) { + if (bridge == nullptr || bridge->metadata() == nullptr || + signatureOffset == MD_SECTION_OFFSET_NULL) { + throw facebook::jsi::JSError( + runtime, "Native method callback metadata is unavailable."); + } + + auto parsed = parseMetadataJsiSignature( + bridge->metadata(), signatureOffset, 2, bridge.get(), returnOwned); + if (!parsed || !signatureSupportedForJsiCallback(*parsed)) { + throw facebook::jsi::JSError( + runtime, "Native method callback signature is not supported by pure JSI."); + } + parsed->selectorName = selectorName; + + auto signature = + std::make_shared(std::move(*parsed)); + auto callback = std::make_shared( + runtime, bridge, std::move(signature), std::move(function), false, true); + bridge->retainJsiLifetime(callback); + return callback; +} + +std::shared_ptr createJsiMethodCallback( + Runtime& runtime, const std::shared_ptr& bridge, + const std::string& selectorName, NativeApiJsiSignature signature, + Function function) { + signature.selectorName = selectorName; + prepareJsiMethodSignature(&signature); + if (!signatureSupportedForJsiCallback(signature)) { + throw facebook::jsi::JSError( + runtime, "Native method callback signature is not supported by pure JSI."); + } + + auto sharedSignature = + std::make_shared(std::move(signature)); + auto callback = std::make_shared( + runtime, bridge, std::move(sharedSignature), std::move(function), false, + true); + bridge->retainJsiLifetime(callback); + return callback; +} diff --git a/NativeScript/ffi/shared/jsi/NativeApiJsiClassBuilder.h b/NativeScript/ffi/shared/jsi/NativeApiJsiClassBuilder.h new file mode 100644 index 00000000..332fccd5 --- /dev/null +++ b/NativeScript/ffi/shared/jsi/NativeApiJsiClassBuilder.h @@ -0,0 +1,788 @@ +std::string readOptionalStringProperty(Runtime& runtime, const Object& object, + const char* name) { + if (name == nullptr || !object.hasProperty(runtime, name)) { + return ""; + } + Value value = object.getProperty(runtime, name); + return value.isString() ? value.asString(runtime).utf8(runtime) : ""; +} + +struct NativeApiJsiClassBuilderRegistration { + std::shared_ptr runtimeOwner; + Runtime* runtime = nullptr; + std::shared_ptr bridge; +}; + +std::mutex gNativeApiJsiClassBuilderMutex; +std::unordered_map + gNativeApiJsiClassBuilders; +struct NativeApiJsiKnownExposedMethod { + std::string selectorName; + NativeApiJsiSignature signature; +}; +std::mutex gNativeApiJsiKnownExposedMethodsMutex; +std::unordered_map + gNativeApiJsiKnownExposedMethods; + +void rememberNativeApiJsiClassBuilder( + Runtime& runtime, const std::shared_ptr& bridge, + Class cls) { + if (cls == Nil) { + return; + } + std::lock_guard lock(gNativeApiJsiClassBuilderMutex); + auto runtimeOwner = retainNativeApiJsiRuntime(runtime); + gNativeApiJsiClassBuilders[cls] = NativeApiJsiClassBuilderRegistration{ + .runtimeOwner = runtimeOwner, + .runtime = runtimeOwner.get(), + .bridge = bridge, + }; +} + +void rememberNativeApiJsiKnownExposedMethod( + const std::string& selectorName, const NativeApiJsiSignature& signature) { + if (selectorName.empty()) { + return; + } + NativeApiJsiKnownExposedMethod method{ + .selectorName = selectorName, + .signature = signature, + }; + std::lock_guard lock(gNativeApiJsiKnownExposedMethodsMutex); + gNativeApiJsiKnownExposedMethods[selectorName] = method; + gNativeApiJsiKnownExposedMethods[jsifySelector(selectorName.c_str())] = + std::move(method); +} + +std::optional knownNativeApiJsiExposedMethod( + const std::string& name) { + std::lock_guard lock(gNativeApiJsiKnownExposedMethodsMutex); + auto it = gNativeApiJsiKnownExposedMethods.find(name); + if (it == gNativeApiJsiKnownExposedMethods.end()) { + return std::nullopt; + } + NativeApiJsiKnownExposedMethod method = it->second; + prepareJsiMethodSignature(&method.signature); + return method; +} + +std::optional +findNativeApiJsiClassBuilder(id object) { + Class cls = object != nil ? object_getClass(object) : Nil; + std::lock_guard lock(gNativeApiJsiClassBuilderMutex); + while (cls != Nil) { + auto it = gNativeApiJsiClassBuilders.find(cls); + if (it != gNativeApiJsiClassBuilders.end()) { + return it->second; + } + cls = class_getSuperclass(cls); + } + return std::nullopt; +} + +const char* nativeApiJsiFastEnumerationEncoding() { + static const char* encoding = nullptr; + if (encoding == nullptr) { + struct objc_method_description desc = protocol_getMethodDescription( + @protocol(NSFastEnumeration), + @selector(countByEnumeratingWithState:objects:count:), YES, YES); + encoding = desc.types; + } + return encoding; +} + +NSUInteger nativeApiJsiSymbolIteratorCountByEnumerating( + id self, SEL, NSFastEnumerationState* state, + id __unsafe_unretained stackbuf[], NSUInteger len) { + if (len == 0 || state == nullptr || stackbuf == nullptr) { + return 0; + } + + auto registration = findNativeApiJsiClassBuilder(self); + if (!registration || registration->runtime == nullptr || + registration->bridge == nullptr) { + return 0; + } + + Runtime& runtime = *registration->runtime; + NativeApiJsiRuntimeScope runtimeScope(runtime); + auto bridge = registration->bridge; + try { + Value receiver = makeNativeObjectValue(runtime, bridge, self, false); + if (!receiver.isObject()) { + return 0; + } + + Value iteratorFactoryValue = + runtime.global().getProperty(runtime, + "__nativeScriptCreateNativeApiIterator"); + if (!iteratorFactoryValue.isObject() || + !iteratorFactoryValue.asObject(runtime).isFunction(runtime)) { + return 0; + } + + Function iteratorFactory = + iteratorFactoryValue.asObject(runtime).asFunction(runtime); + Value prototype = + bridge->findClassPrototype(runtime, object_getClass(self)); + Value iteratorValue = + prototype.isObject() + ? iteratorFactory.call(runtime, Value(runtime, receiver), + Value(runtime, prototype)) + : iteratorFactory.call(runtime, Value(runtime, receiver)); + if (!iteratorValue.isObject()) { + return 0; + } + Object iterator = iteratorValue.asObject(runtime); + Value nextValue = iterator.getProperty(runtime, "next"); + if (!nextValue.isObject() || + !nextValue.asObject(runtime).isFunction(runtime)) { + return 0; + } + Function next = nextValue.asObject(runtime).asFunction(runtime); + + auto callNext = [&]() -> Value { + return next.callWithThis(runtime, iterator); + }; + + for (unsigned long skipped = 0; skipped < state->state; skipped++) { + Value skippedResult = callNext(); + if (!skippedResult.isObject()) { + return 0; + } + Value doneValue = + skippedResult.asObject(runtime).getProperty(runtime, "done"); + if (doneValue.isBool() && doneValue.getBool()) { + return 0; + } + } + + NSUInteger count = 0; + while (count < len) { + Value nextResult = callNext(); + if (!nextResult.isObject()) { + break; + } + Object nextObject = nextResult.asObject(runtime); + Value doneValue = nextObject.getProperty(runtime, "done"); + if (doneValue.isBool() && doneValue.getBool()) { + break; + } + + Value value = nextObject.getProperty(runtime, "value"); + NativeApiJsiArgumentFrame frame(1); + id nativeValue = objectFromJsiValue(runtime, bridge, value, frame, false); + if (nativeValue != nil) { + [nativeValue retain]; + [nativeValue autorelease]; + } + stackbuf[count++] = nativeValue; + } + + state->itemsPtr = stackbuf; + state->mutationsPtr = &state->extra[0]; + state->extra[0] = 0; + state->state += count; + return count; + } catch (const std::exception&) { + return 0; + } +} + +NativeApiSymbol runtimeSymbolForClass( + const std::shared_ptr& bridge, Class cls) { + if (bridge != nullptr) { + if (const NativeApiSymbol* symbol = bridge->findClassForRuntimeClass(cls)) { + return *symbol; + } + } + + const char* name = cls != Nil ? class_getName(cls) : ""; + return NativeApiSymbol{ + .kind = NativeApiSymbolKind::Class, + .offset = MD_SECTION_OFFSET_NULL, + .name = name != nullptr ? name : "", + .runtimeName = name != nullptr ? name : "", + }; +} + +std::string nextAvailableJsiClassName(const std::string& requestedName) { + if (requestedName.empty()) { + return ""; + } + if (objc_lookUpClass(requestedName.c_str()) == Nil) { + return requestedName; + } + + size_t suffix = 1; + std::string candidate; + do { + candidate = requestedName + std::to_string(suffix++); + } while (objc_lookUpClass(candidate.c_str()) != Nil); + return candidate; +} + +std::vector methodOverridesForName( + const std::vector& members, const std::string& name) { + std::vector result; + std::unordered_set selectors; + for (const auto& member : members) { + if (member.property || member.name != name || + (member.flags & metagen::mdMemberStatic) != 0 || + member.selectorName.empty()) { + continue; + } + if (selectors.insert(member.selectorName).second) { + result.push_back(member); + } + } + return result; +} + +const NativeApiMember* propertyOverrideForName( + const std::vector& members, const std::string& name) { + const NativeApiMember* fallback = nullptr; + for (const auto& member : members) { + if (member.property && member.name == name && + (member.flags & metagen::mdMemberStatic) == 0) { + if (fallback == nullptr) { + fallback = &member; + } + if (!member.readonly && !member.setterSelectorName.empty()) { + return &member; + } + } + } + return fallback; +} + +void addJsiOverrideMethod(Runtime& runtime, + const std::shared_ptr& bridge, + Class nativeClass, Class baseClass, + const std::string& selectorName, + MDSectionOffset signatureOffset, + bool returnOwned, Function function) { + if (selectorName.empty() || signatureOffset == MD_SECTION_OFFSET_NULL) { + return; + } + + auto callback = createJsiMethodCallback(runtime, bridge, selectorName, + signatureOffset, std::move(function), + returnOwned); + SEL selector = sel_registerName(selectorName.c_str()); + std::string metadataEncoding = + objcMethodSignatureForJsiSignature(callback->signature()); + class_replaceMethod(nativeClass, selector, + reinterpret_cast(callback->functionPointer()), + metadataEncoding.c_str()); +} + +Value getObjectPropertyOrUndefined(Runtime& runtime, const Object& object, + const std::string& name) { + return object.hasProperty(runtime, name.c_str()) + ? object.getProperty(runtime, name.c_str()) + : Value::undefined(); +} + +Class dispatchSuperclassForJsiDerivedReceiver(id receiver, Class fallback) { + if (receiver == nil) { + return Nil; + } + + Class receiverClass = object_getClass(receiver); + if (receiverClass == Nil || + !class_conformsToProtocol(receiverClass, + @protocol(NativeApiJsiClassBuilderProtocol))) { + return Nil; + } + + Class superclass = class_getSuperclass(receiverClass); + return superclass != Nil ? superclass : fallback; +} + +std::optional functionForSelector(Runtime& runtime, + const Object& methods, + const std::string& selectorName) { + Value value = getObjectPropertyOrUndefined(runtime, methods, selectorName); + if (!value.isObject() || !value.asObject(runtime).isFunction(runtime)) { + std::string jsName = jsifySelector(selectorName.c_str()); + if (jsName != selectorName) { + value = getObjectPropertyOrUndefined(runtime, methods, jsName); + } + } + if (!value.isObject() || !value.asObject(runtime).isFunction(runtime)) { + return std::nullopt; + } + return value.asObject(runtime).asFunction(runtime); +} + +std::optional readExposedType( + Runtime& runtime, const std::shared_ptr& bridge, + const Object& descriptor, const char* propertyName) { + if (!descriptor.hasProperty(runtime, propertyName)) { + return std::nullopt; + } + return interopTypeFromValue(runtime, bridge, + descriptor.getProperty(runtime, propertyName)); +} + +std::optional exposedMethodSignature( + Runtime& runtime, const std::shared_ptr& bridge, + const std::string& selectorName, const Object& descriptor) { + NativeApiJsiSignature signature; + if (auto returnType = readExposedType(runtime, bridge, descriptor, "returns")) { + signature.returnType = *returnType; + } else { + signature.returnType = primitiveInteropType(metagen::mdTypeVoid); + } + + Value paramsValue = getObjectPropertyOrUndefined(runtime, descriptor, "params"); + if (!paramsValue.isUndefined() && !paramsValue.isNull()) { + if (!paramsValue.isObject() || !paramsValue.asObject(runtime).isArray(runtime)) { + throw facebook::jsi::JSError( + runtime, "exposedMethods params must be an array."); + } + Array params = paramsValue.asObject(runtime).getArray(runtime); + for (size_t i = 0; i < params.size(runtime); i++) { + Value typeValue = params.getValueAtIndex(runtime, i); + auto type = interopTypeFromValue(runtime, bridge, typeValue); + if (!type) { + throw facebook::jsi::JSError( + runtime, "exposedMethods contains an unsupported parameter type."); + } + signature.argumentTypes.push_back(*type); + } + } + + if (selectorArgumentCount(selectorName) != signature.argumentTypes.size()) { + throw facebook::jsi::JSError( + runtime, "exposedMethods selector argument count does not match params."); + } + + prepareJsiMethodSignature(&signature); + return signature; +} + +std::optional runtimeProtocolMethodSignature( + const char* types) { + if (types == nullptr) { + return std::nullopt; + } + + NSMethodSignature* methodSignature = + [NSMethodSignature signatureWithObjCTypes:types]; + if (methodSignature == nil || methodSignature.numberOfArguments < 2) { + return std::nullopt; + } + + NativeApiJsiSignature signature; + signature.implicitArgumentCount = 2; + signature.returnType = + parseObjCEncodedJsiType(methodSignature.methodReturnType); + for (NSUInteger i = 2; i < methodSignature.numberOfArguments; i++) { + signature.argumentTypes.push_back( + parseObjCEncodedJsiType([methodSignature getArgumentTypeAtIndex:i])); + } + if (unsupportedJsiType(signature.returnType)) { + return std::nullopt; + } + for (const auto& argumentType : signature.argumentTypes) { + if (unsupportedJsiType(argumentType)) { + return std::nullopt; + } + } + return signature; +} + +std::optional protocolSymbolFromJsiValue( + Runtime& runtime, const std::shared_ptr& bridge, + const Value& value) { + if (value.isString()) { + std::string name = value.asString(runtime).utf8(runtime); + if (const NativeApiSymbol* symbol = bridge->findProtocol(name)) { + return *symbol; + } + return std::nullopt; + } + if (!value.isObject()) { + return std::nullopt; + } + + Object object = value.asObject(runtime); + if (object.isHostObject(runtime)) { + return object.getHostObject(runtime)->symbol(); + } + + if (stringPropertyOrEmpty(runtime, object, "kind") != "protocol") { + return std::nullopt; + } + + std::string runtimeName = stringPropertyOrEmpty(runtime, object, "runtimeName"); + if (!runtimeName.empty()) { + if (const NativeApiSymbol* symbol = bridge->findProtocol(runtimeName)) { + return *symbol; + } + } + + std::string name = stringPropertyOrEmpty(runtime, object, "name"); + if (!name.empty()) { + if (const NativeApiSymbol* symbol = bridge->findProtocol(name)) { + return *symbol; + } + } + + return std::nullopt; +} + +void addJsiExposedMethod(Runtime& runtime, + const std::shared_ptr& bridge, + Class nativeClass, const std::string& selectorName, + NativeApiJsiSignature signature, Function function) { + if (selectorName.empty()) { + return; + } + auto callback = createJsiMethodCallback(runtime, bridge, selectorName, + std::move(signature), std::move(function)); + std::string encoding = objcMethodSignatureForJsiSignature(callback->signature()); + class_replaceMethod(nativeClass, sel_registerName(selectorName.c_str()), + reinterpret_cast(callback->functionPointer()), + encoding.c_str()); +} + +bool addRuntimeProtocolOverrideForName( + Runtime& runtime, const std::shared_ptr& bridge, + Class nativeClass, const std::vector& protocols, + const std::string& propertyName, Function function) { + std::unordered_set visited; + std::function visit = [&](Protocol* protocol) -> bool { + if (protocol == nullptr || !visited.insert(protocol).second) { + return false; + } + + Protocol** inherited = protocol_copyProtocolList(protocol, nullptr); + if (inherited != nullptr) { + unsigned int inheritedCount = 0; + free(inherited); + inherited = protocol_copyProtocolList(protocol, &inheritedCount); + for (unsigned int i = 0; i < inheritedCount; i++) { + if (visit(inherited[i])) { + free(inherited); + return true; + } + } + free(inherited); + } + + for (BOOL required : {YES, NO}) { + unsigned int count = 0; + objc_method_description* descriptions = + protocol_copyMethodDescriptionList(protocol, required, YES, &count); + for (unsigned int i = 0; i < count; i++) { + SEL selector = descriptions[i].name; + const char* selectorName = + selector != nullptr ? sel_getName(selector) : nullptr; + if (selectorName == nullptr || + jsifySelector(selectorName) != propertyName) { + continue; + } + auto signature = runtimeProtocolMethodSignature(descriptions[i].types); + if (signature) { + addJsiExposedMethod(runtime, bridge, nativeClass, selectorName, + std::move(*signature), std::move(function)); + free(descriptions); + return true; + } + } + free(descriptions); + } + return false; + }; + + for (Protocol* protocol : protocols) { + if (visit(protocol)) { + return true; + } + } + return false; +} + +Object getOwnPropertyDescriptor(Runtime& runtime, const Object& object, + const std::string& name) { + Object objectCtor = runtime.global().getPropertyAsObject(runtime, "Object"); + Function getOwnPropertyDescriptor = + objectCtor.getPropertyAsFunction(runtime, "getOwnPropertyDescriptor"); + Value args[] = {Value(runtime, object), makeString(runtime, name)}; + Value descriptorValue = + getOwnPropertyDescriptor.call(runtime, static_cast(args), + static_cast(2)); + return descriptorValue.isObject() ? descriptorValue.asObject(runtime) + : Object(runtime); +} + +Value extendNativeApiJsiClass( + Runtime& runtime, const std::shared_ptr& bridge, + const Value* args, size_t count) { + if (count < 2 || !args[0].isObject() || !args[1].isObject()) { + throw facebook::jsi::JSError( + runtime, "extendClass expects a native class and method object."); + } + + Class baseClass = classFromJsiValue(runtime, args[0]); + if (baseClass == Nil) { + throw facebook::jsi::JSError( + runtime, "extendClass can only extend native class constructors."); + } + if (class_conformsToProtocol(baseClass, + @protocol(NativeApiJsiClassBuilderProtocol))) { + throw facebook::jsi::JSError(runtime, + "Cannot extend an already extended class."); + } + + Object methods = args[1].asObject(runtime); + Object options = count >= 3 && args[2].isObject() + ? args[2].asObject(runtime) + : Object(runtime); + std::string requestedName = readOptionalStringProperty(runtime, options, "name"); + if (requestedName.empty()) { + const char* baseName = class_getName(baseClass); + requestedName = std::string(baseName != nullptr ? baseName : "NSObject") + + "_Extended_" + std::to_string(rand()); + } + + std::string className = nextAvailableJsiClassName(requestedName); + Class nativeClass = objc_allocateClassPair(baseClass, className.c_str(), 0); + if (nativeClass == Nil) { + throw facebook::jsi::JSError(runtime, "Failed to allocate Objective-C class."); + } + + class_addProtocol(nativeClass, @protocol(NativeApiJsiClassBuilderProtocol)); + rememberNativeApiJsiClassBuilder(runtime, bridge, nativeClass); + + NativeApiSymbol baseSymbol = runtimeSymbolForClass(bridge, baseClass); + std::vector extensionMembers = + bridge->membersForClass(baseSymbol); + std::vector optionProtocols; + Value protocolsValue = getObjectPropertyOrUndefined(runtime, options, "protocols"); + if (protocolsValue.isObject() && + protocolsValue.asObject(runtime).isArray(runtime)) { + Array protocols = protocolsValue.asObject(runtime).getArray(runtime); + for (size_t i = 0; i < protocols.size(runtime); i++) { + Value protocolValue = protocols.getValueAtIndex(runtime, i); + Protocol* protocol = protocolFromJsiValue(runtime, protocolValue); + std::optional protocolSymbol = + protocolSymbolFromJsiValue(runtime, bridge, protocolValue); + if (protocol != nullptr) { + optionProtocols.push_back(protocol); + class_addProtocol(nativeClass, protocol); + if (!protocolSymbol) { + if (const NativeApiSymbol* runtimeSymbol = + bridge->findProtocolForRuntimePointer(protocol)) { + protocolSymbol = *runtimeSymbol; + } + } + } + if (protocolSymbol) { + const auto& protocolMembers = bridge->membersForProtocol(*protocolSymbol); + extensionMembers.insert(extensionMembers.begin(), + protocolMembers.begin(), + protocolMembers.end()); + } + } + } + const auto& members = extensionMembers; + Array propertyNames = methods.getPropertyNames(runtime); + for (size_t i = 0; i < propertyNames.size(runtime); i++) { + Value propertyNameValue = propertyNames.getValueAtIndex(runtime, i); + if (!propertyNameValue.isString()) { + continue; + } + + std::string propertyName = propertyNameValue.asString(runtime).utf8(runtime); + Object descriptor = getOwnPropertyDescriptor(runtime, methods, propertyName); + + Value value = descriptor.getProperty(runtime, "value"); + if (value.isObject() && value.asObject(runtime).isFunction(runtime)) { + auto overrides = methodOverridesForName(members, propertyName); + bool addedOverride = false; + for (const auto& member : overrides) { + if (member.selectorName.empty() || + member.signatureOffset == MD_SECTION_OFFSET_NULL || + member.signatureOffset == 0) { + continue; + } + addJsiOverrideMethod( + runtime, bridge, nativeClass, baseClass, member.selectorName, + member.signatureOffset, + (member.flags & metagen::mdMemberReturnOwned) != 0, + value.asObject(runtime).asFunction(runtime)); + addedOverride = true; + } + if (!addedOverride) { + bool addedRuntimeProtocolOverride = addRuntimeProtocolOverrideForName( + runtime, bridge, nativeClass, optionProtocols, propertyName, + value.asObject(runtime).asFunction(runtime)); + if (!addedRuntimeProtocolOverride) { + if (auto known = knownNativeApiJsiExposedMethod(propertyName)) { + addJsiExposedMethod(runtime, bridge, nativeClass, + known->selectorName, + std::move(known->signature), + value.asObject(runtime).asFunction(runtime)); + } + } + } + } + + const NativeApiMember* propertyMember = + propertyOverrideForName(members, propertyName); + + Value getter = descriptor.getProperty(runtime, "get"); + if (propertyMember != nullptr && getter.isObject() && + getter.asObject(runtime).isFunction(runtime)) { + addJsiOverrideMethod( + runtime, bridge, nativeClass, baseClass, + propertyMember->selectorName, propertyMember->signatureOffset, + (propertyMember->flags & metagen::mdMemberReturnOwned) != 0, + getter.asObject(runtime).asFunction(runtime)); + } else if (propertyMember == nullptr && getter.isObject() && + getter.asObject(runtime).isFunction(runtime)) { + auto overrides = methodOverridesForName(members, propertyName); + for (const auto& member : overrides) { + if (selectorArgumentCount(member.selectorName) != 0) { + continue; + } + addJsiOverrideMethod( + runtime, bridge, nativeClass, baseClass, member.selectorName, + member.signatureOffset, + (member.flags & metagen::mdMemberReturnOwned) != 0, + getter.asObject(runtime).asFunction(runtime)); + } + } + + Value setter = descriptor.getProperty(runtime, "set"); + if (propertyMember != nullptr && + setter.isObject() && setter.asObject(runtime).isFunction(runtime) && + !propertyMember->setterSelectorName.empty()) { + addJsiOverrideMethod(runtime, bridge, nativeClass, baseClass, + propertyMember->setterSelectorName, + propertyMember->setterSignatureOffset, false, + setter.asObject(runtime).asFunction(runtime)); + } + } + + Value exposedMethodsValue = + getObjectPropertyOrUndefined(runtime, options, "exposedMethods"); + if (!exposedMethodsValue.isObject()) { + exposedMethodsValue = + getObjectPropertyOrUndefined(runtime, methods, "ObjCExposedMethods"); + } + if (exposedMethodsValue.isObject()) { + Object exposedMethods = exposedMethodsValue.asObject(runtime); + Array exposedNames = exposedMethods.getPropertyNames(runtime); + for (size_t i = 0; i < exposedNames.size(runtime); i++) { + Value selectorValue = exposedNames.getValueAtIndex(runtime, i); + if (!selectorValue.isString()) { + continue; + } + std::string selectorName = selectorValue.asString(runtime).utf8(runtime); + Value descriptorValue = + getObjectPropertyOrUndefined(runtime, exposedMethods, selectorName); + if (!descriptorValue.isObject()) { + continue; + } + auto function = functionForSelector(runtime, methods, selectorName); + if (!function) { + continue; + } + auto signature = exposedMethodSignature( + runtime, bridge, selectorName, descriptorValue.asObject(runtime)); + if (signature) { + rememberNativeApiJsiKnownExposedMethod(selectorName, *signature); + addJsiExposedMethod(runtime, bridge, nativeClass, selectorName, + std::move(*signature), std::move(*function)); + } + } + } + + Value hasIteratorValue = + getObjectPropertyOrUndefined(runtime, options, "__hasIterator"); + if (hasIteratorValue.isBool() && hasIteratorValue.getBool()) { + class_addProtocol(nativeClass, @protocol(NSFastEnumeration)); + if (const char* encoding = nativeApiJsiFastEnumerationEncoding()) { + class_replaceMethod( + nativeClass, + @selector(countByEnumeratingWithState:objects:count:), + reinterpret_cast(nativeApiJsiSymbolIteratorCountByEnumerating), + encoding); + } + } + + objc_registerClassPair(nativeClass); + + NativeApiSymbol newSymbol = baseSymbol; + newSymbol.name = className; + newSymbol.runtimeName = className; + newSymbol.superclassOffset = baseSymbol.offset; + return makeNativeClassValue(runtime, bridge, std::move(newSymbol)); + } + +Value invokeNativeApiJsiBaseMethod( + Runtime& runtime, const std::shared_ptr& bridge, + const Value* args, size_t count) { + if (count < 3 || !args[0].isObject() || !args[1].isObject() || + !args[2].isString()) { + throw facebook::jsi::JSError( + runtime, "__invokeBase expects base class, receiver, and member name."); + } + + Class baseClass = classFromJsiValue(runtime, args[0]); + if (baseClass == Nil) { + throw facebook::jsi::JSError(runtime, "__invokeBase base class is invalid."); + } + + Object receiverObject = args[1].asObject(runtime); + if (!receiverObject.isHostObject(runtime)) { + throw facebook::jsi::JSError(runtime, "__invokeBase receiver is not native."); + } + + id receiver = + receiverObject.getHostObject(runtime)->object(); + std::string memberName = args[2].asString(runtime).utf8(runtime); + size_t actualArgc = count - 3; + + NativeApiSymbol baseSymbol = runtimeSymbolForClass(bridge, baseClass); + const auto& members = bridge->membersForClass(baseSymbol); + const NativeApiMember* member = + selectMethodMember(members, memberName, false, actualArgc); + if (member == nullptr) { + if (const NativeApiMember* propertyMember = + selectWritablePropertyMember(members, memberName, false)) { + if (actualArgc == 0) { + Class dispatchClass = + dispatchSuperclassForJsiDerivedReceiver(receiver, baseClass); + return callObjCSelector(runtime, bridge, receiver, false, + propertyMember->selectorName, propertyMember, + nullptr, 0, dispatchClass); + } + if (actualArgc == 1 && !propertyMember->setterSelectorName.empty() && + !propertyMember->readonly) { + Class dispatchClass = + dispatchSuperclassForJsiDerivedReceiver(receiver, baseClass); + NativeApiMember setterMember = *propertyMember; + setterMember.selectorName = propertyMember->setterSelectorName; + setterMember.signatureOffset = propertyMember->setterSignatureOffset; + return callObjCSelector(runtime, bridge, receiver, false, + setterMember.selectorName, &setterMember, + args + 3, actualArgc, dispatchClass); + } + } + } + if (member == nullptr) { + throw facebook::jsi::JSError( + runtime, "Objective-C base selector is not available: " + memberName); + } + + Class dispatchClass = + dispatchSuperclassForJsiDerivedReceiver(receiver, baseClass); + return callObjCSelector(runtime, bridge, receiver, false, member->selectorName, + member, args + 3, actualArgc, dispatchClass); +} diff --git a/NativeScript/ffi/shared/jsi/NativeApiJsiConversion.h b/NativeScript/ffi/shared/jsi/NativeApiJsiConversion.h new file mode 100644 index 00000000..61e0c346 --- /dev/null +++ b/NativeScript/ffi/shared/jsi/NativeApiJsiConversion.h @@ -0,0 +1,2097 @@ +std::string stringPropertyOrEmpty(Runtime& runtime, const Object& object, + const char* name); +void* pointerFromSymbolLikeObject(Runtime& runtime, const Object& object); + +id objectFromJsiValue(Runtime& runtime, + const std::shared_ptr& bridge, + const Value& value, NativeApiJsiArgumentFrame& frame, + bool mutableString) { + if (value.isNull() || value.isUndefined()) { + return nil; + } + if (value.isString()) { + std::string utf8 = value.asString(runtime).utf8(runtime); + id string = mutableString + ? [[NSMutableString alloc] initWithBytes:utf8.data() + length:utf8.size() + encoding:NSUTF8StringEncoding] + : [[NSString alloc] initWithBytes:utf8.data() + length:utf8.size() + encoding:NSUTF8StringEncoding]; + frame.addObject(string); + return string; + } + if (value.isBool()) { + return [NSNumber numberWithBool:value.getBool()]; + } + if (value.isNumber()) { + return [NSNumber numberWithDouble:value.getNumber()]; + } + if (value.isObject()) { + Object object = value.asObject(runtime); + if (object.isHostObject(runtime)) { + return object.getHostObject(runtime)->object(); + } + if (Class cls = nativeClassFromJsiObject(runtime, object)) { + return static_cast(cls); + } + if (object.isHostObject(runtime)) { + return static_cast( + object.getHostObject(runtime) + ->nativeProtocol()); + } + if (void* symbolPointer = pointerFromSymbolLikeObject(runtime, object)) { + return static_cast(symbolPointer); + } + if (object.isHostObject(runtime)) { + return static_cast( + object.getHostObject(runtime)->pointer()); + } + if (object.isHostObject(runtime)) { + return static_cast( + object.getHostObject(runtime)->data()); + } + if (object.isHostObject(runtime)) { + return static_cast( + object.getHostObject(runtime)->data()); + } + + Value getTimeValue = object.getProperty(runtime, "getTime"); + Value toISOStringValue = object.getProperty(runtime, "toISOString"); + if (getTimeValue.isObject() && + getTimeValue.asObject(runtime).isFunction(runtime) && + toISOStringValue.isObject() && + toISOStringValue.asObject(runtime).isFunction(runtime)) { + Value millisValue = getTimeValue.asObject(runtime) + .asFunction(runtime) + .callWithThis(runtime, object, nullptr, 0); + if (millisValue.isNumber()) { + NSDate* date = [NSDate dateWithTimeIntervalSince1970:millisValue.getNumber() / 1000.0]; + bridge->rememberRoundTripValue(runtime, date, value); + return date; + } + } + + Value valueOfValue = object.getProperty(runtime, "valueOf"); + if (valueOfValue.isObject() && + valueOfValue.asObject(runtime).isFunction(runtime)) { + Value primitiveValue = valueOfValue.asObject(runtime) + .asFunction(runtime) + .callWithThis(runtime, object, nullptr, 0); + if (primitiveValue.isString() || primitiveValue.isBool() || + primitiveValue.isNumber()) { + return objectFromJsiValue(runtime, bridge, primitiveValue, frame, + mutableString); + } + } + + const uint8_t* bytes = nullptr; + size_t byteLength = 0; + if (readJsiBuffer(runtime, object, &bytes, &byteLength)) { + NSData* data = [NSData dataWithBytes:bytes length:byteLength]; + bridge->rememberRoundTripValue(runtime, data, value); + return data; + } + + if (object.isArray(runtime)) { + Array array = object.getArray(runtime); + NSMutableArray* nativeArray = + [NSMutableArray arrayWithCapacity:array.size(runtime)]; + for (size_t i = 0; i < array.size(runtime); i++) { + id element = objectFromJsiValue(runtime, bridge, + array.getValueAtIndex(runtime, i), + frame, false); + [nativeArray addObject:element != nil ? element : [NSNull null]]; + } + bridge->rememberRoundTripValue(runtime, nativeArray, value); + return nativeArray; + } + + Value lengthValue = object.getProperty(runtime, "length"); + if (lengthValue.isNumber() && std::isfinite(lengthValue.getNumber()) && + lengthValue.getNumber() >= 0) { + size_t length = static_cast(std::floor(lengthValue.getNumber())); + NSMutableArray* nativeArray = [NSMutableArray arrayWithCapacity:length]; + for (size_t i = 0; i < length; i++) { + std::string key = std::to_string(i); + id element = objectFromJsiValue( + runtime, bridge, object.getProperty(runtime, key.c_str()), frame, + false); + [nativeArray addObject:element != nil ? element : [NSNull null]]; + } + bridge->rememberRoundTripValue(runtime, nativeArray, value); + return nativeArray; + } + + Value entriesValue = object.getProperty(runtime, "entries"); + Value sizeValue = object.getProperty(runtime, "size"); + Value getValue = object.getProperty(runtime, "get"); + if (entriesValue.isObject() && + entriesValue.asObject(runtime).isFunction(runtime) && + sizeValue.isNumber() && getValue.isObject() && + getValue.asObject(runtime).isFunction(runtime)) { + Object arrayCtor = runtime.global().getPropertyAsObject(runtime, "Array"); + Function arrayFrom = arrayCtor.getPropertyAsFunction(runtime, "from"); + Value iterator = entriesValue.asObject(runtime) + .asFunction(runtime) + .callWithThis(runtime, object, nullptr, 0); + Value pairsValue = arrayFrom.call(runtime, iterator); + if (pairsValue.isObject() && pairsValue.asObject(runtime).isArray(runtime)) { + Array pairs = pairsValue.asObject(runtime).getArray(runtime); + NSMutableDictionary* nativeMap = + [NSMutableDictionary dictionaryWithCapacity:pairs.size(runtime)]; + for (size_t i = 0; i < pairs.size(runtime); i++) { + Value pairValue = pairs.getValueAtIndex(runtime, i); + if (!pairValue.isObject() || + !pairValue.asObject(runtime).isArray(runtime)) { + continue; + } + Array pair = pairValue.asObject(runtime).getArray(runtime); + if (pair.size(runtime) < 2) { + continue; + } + id key = objectFromJsiValue(runtime, bridge, + pair.getValueAtIndex(runtime, 0), + frame, false); + id nativeValue = objectFromJsiValue(runtime, bridge, + pair.getValueAtIndex(runtime, 1), + frame, false); + if (key != nil) { + [nativeMap setObject:nativeValue != nil ? nativeValue : [NSNull null] + forKey:key]; + } + } + bridge->rememberRoundTripValue(runtime, nativeMap, value); + return nativeMap; + } + } + + NSMutableDictionary* dictionary = [NSMutableDictionary dictionary]; + Array propertyNames = object.getPropertyNames(runtime); + for (size_t i = 0; i < propertyNames.size(runtime); i++) { + Value propertyNameValue = propertyNames.getValueAtIndex(runtime, i); + if (!propertyNameValue.isString()) { + continue; + } + std::string key = propertyNameValue.asString(runtime).utf8(runtime); + Value propertyValue = object.getProperty(runtime, key.c_str()); + if (propertyValue.isUndefined()) { + continue; + } + id nativeValue = + objectFromJsiValue(runtime, bridge, propertyValue, frame, false); + NSString* nativeKey = [NSString stringWithUTF8String:key.c_str()]; + if (nativeKey != nil) { + [dictionary setObject:nativeValue != nil ? nativeValue : [NSNull null] + forKey:nativeKey]; + } + } + bridge->rememberRoundTripValue(runtime, dictionary, value); + return dictionary; + } + throw facebook::jsi::JSError(runtime, + "Value cannot be converted to Objective-C object."); +} + +std::string utf8StringFromNSString(NSString* string) { + if (string == nil) { + return ""; + } + NSUInteger length = [string lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; + std::string result(length, '\0'); + NSUInteger usedLength = 0; + NSRange remainingRange = NSMakeRange(0, 0); + BOOL ok = [string getBytes:result.data() + maxLength:length + usedLength:&usedLength + encoding:NSUTF8StringEncoding + options:0 + range:NSMakeRange(0, string.length) + remainingRange:&remainingRange]; + if (!ok) { + return string.UTF8String ?: ""; + } + result.resize(usedLength); + return result; +} + +bool readNativePointerProperty(Runtime& runtime, const Object& object, + void** pointer) { + if (pointer == nullptr) { + return false; + } + + Value nativePointerObjectValue = + object.getProperty(runtime, "__nativeApiPointerObject"); + if (nativePointerObjectValue.isObject()) { + Object nativePointerObject = nativePointerObjectValue.asObject(runtime); + if (nativePointerObject.isHostObject( + runtime)) { + *pointer = nativePointerObject + .getHostObject(runtime) + ->pointer(); + return true; + } + } + + Value nativePointerValue = + object.getProperty(runtime, "__nativeApiPointer"); + if (nativePointerValue.isNumber()) { + *pointer = reinterpret_cast( + static_cast(nativePointerValue.getNumber())); + return true; + } + + Value nativeAddressValue = object.getProperty(runtime, "nativeAddress"); + if (nativeAddressValue.isNumber()) { + *pointer = reinterpret_cast( + static_cast(nativeAddressValue.getNumber())); + return true; + } + + return false; +} + +std::string stringPropertyOrEmpty(Runtime& runtime, const Object& object, + const char* name) { + if (name == nullptr || !object.hasProperty(runtime, name)) { + return ""; + } + Value value = object.getProperty(runtime, name); + return value.isString() ? value.asString(runtime).utf8(runtime) : ""; +} + +void* pointerFromSymbolLikeObject(Runtime& runtime, const Object& object) { + std::string kind = stringPropertyOrEmpty(runtime, object, "kind"); + if (kind != "class" && kind != "protocol") { + return nullptr; + } + + std::string runtimeName = stringPropertyOrEmpty(runtime, object, "runtimeName"); + if (runtimeName.empty()) { + runtimeName = stringPropertyOrEmpty(runtime, object, "name"); + } + if (runtimeName.empty()) { + return nullptr; + } + + if (kind == "class") { + return objc_lookUpClass(runtimeName.c_str()); + } + return lookupProtocolByNativeName(runtimeName); +} + +void* pointerFromJsiValue(Runtime& runtime, const Value& value, + NativeApiJsiArgumentFrame& frame) { + if (value.isNull() || value.isUndefined()) { + return nullptr; + } + if (value.isNumber()) { + return reinterpret_cast(static_cast(value.getNumber())); + } + if (value.isObject()) { + Object object = value.asObject(runtime); + if (object.isHostObject(runtime)) { + return object.getHostObject(runtime)->pointer(); + } + if (object.isHostObject(runtime)) { + return object.getHostObject(runtime)->object(); + } + if (Class cls = nativeClassFromJsiObject(runtime, object)) { + return cls; + } + if (object.isHostObject(runtime)) { + return object.getHostObject(runtime) + ->nativeProtocol(); + } + if (void* symbolPointer = pointerFromSymbolLikeObject(runtime, object)) { + return symbolPointer; + } + if (object.isHostObject(runtime)) { + auto reference = + object.getHostObject(runtime); + if (reference->data() == nullptr) { + reference->ensureStorage(runtime, reference->type(), frame); + } + return reference->data(); + } + if (object.isHostObject(runtime)) { + return object.getHostObject(runtime)->data(); + } + void* nativePointer = nullptr; + if (readNativePointerProperty(runtime, object, &nativePointer)) { + return nativePointer; + } + const uint8_t* bytes = nullptr; + size_t byteLength = 0; + if (readJsiBuffer(runtime, object, &bytes, &byteLength)) { + return const_cast(bytes); + } + } + if (value.isString()) { + std::string utf8 = value.asString(runtime).utf8(runtime); + char* string = strdup(utf8.c_str()); + return string; + } + throw facebook::jsi::JSError(runtime, "Value cannot be converted to pointer."); +} + +bool readPointerLikeValue(Runtime& runtime, const Value& value, void** pointer) { + if (pointer == nullptr || !value.isObject()) { + return false; + } + Object object = value.asObject(runtime); + if (object.isHostObject(runtime)) { + *pointer = object.getHostObject(runtime)->pointer(); + return true; + } + if (object.isHostObject(runtime)) { + *pointer = object.getHostObject(runtime)->data(); + return true; + } + if (object.isHostObject(runtime)) { + *pointer = object.getHostObject(runtime)->data(); + return true; + } + if (object.isHostObject(runtime)) { + *pointer = object.getHostObject(runtime)->object(); + return true; + } + if (Class cls = nativeClassFromJsiObject(runtime, object)) { + *pointer = cls; + return true; + } + if (object.isHostObject(runtime)) { + *pointer = + object.getHostObject(runtime)->nativeProtocol(); + return true; + } + if (void* symbolPointer = pointerFromSymbolLikeObject(runtime, object)) { + *pointer = symbolPointer; + return true; + } + return readNativePointerProperty(runtime, object, pointer); +} + +template +void writeNumericArgument(Runtime& runtime, const Value& value, void* target, + const char* typeName) { + const Value* numericValue = &value; + Value primitiveValue = Value::undefined(); + if (value.isObject()) { + Object object = value.asObject(runtime); + Value valueOfValue = object.getProperty(runtime, "valueOf"); + if (valueOfValue.isObject() && + valueOfValue.asObject(runtime).isFunction(runtime)) { + primitiveValue = valueOfValue.asObject(runtime) + .asFunction(runtime) + .callWithThis(runtime, object, nullptr, 0); + numericValue = &primitiveValue; + } + } + + if (!numericValue->isNumber() && !numericValue->isBool()) { + throw facebook::jsi::JSError(runtime, + std::string("Expected numeric ") + typeName + + " argument."); + } + double number = numericValue->isBool() ? (numericValue->getBool() ? 1.0 : 0.0) + : numericValue->getNumber(); + *static_cast(target) = static_cast(number); +} + +void convertJsiArgument(Runtime& runtime, + const std::shared_ptr& bridge, + const NativeApiJsiType& type, + const Value& value, void* target, + NativeApiJsiArgumentFrame& frame); + +Value convertNativeReturnValue(Runtime& runtime, + const std::shared_ptr& bridge, + const NativeApiJsiType& type, void* value); + +Class classFromJsiValue(Runtime& runtime, const Value& value); +Protocol* protocolFromJsiValue(Runtime& runtime, const Value& value); + +std::optional parseArrayIndexProperty(const std::string& property) { + if (property.empty()) { + return std::nullopt; + } + size_t index = 0; + for (char c : property) { + if (!std::isdigit(static_cast(c))) { + return std::nullopt; + } + size_t digit = static_cast(c - '0'); + if (index > (std::numeric_limits::max() - digit) / 10) { + return std::nullopt; + } + index = (index * 10) + digit; + } + return index; +} + +size_t referenceElementStride(const NativeApiJsiType& type) { + return std::max(nativeSizeForType(type), 1); +} + +void convertAggregateArgument(Runtime& runtime, + const std::shared_ptr& bridge, + const NativeApiJsiType& type, + const Value& value, void* target, + NativeApiJsiArgumentFrame& frame) { + size_t size = nativeSizeForType(type); + if (size == 0) { + return; + } + + std::memset(target, 0, size); + if (value.isNull() || value.isUndefined()) { + return; + } + + if (value.isObject()) { + Object object = value.asObject(runtime); + if (object.isHostObject(runtime)) { + auto structObject = object.getHostObject(runtime); + if (structObject->data() != nullptr) { + std::memcpy(target, structObject->data(), + std::min(size, static_cast(structObject->info()->size))); + } + return; + } + if (object.isHostObject(runtime)) { + void* data = object.getHostObject(runtime)->data(); + if (data != nullptr) { + std::memcpy(target, data, size); + } + return; + } + if (object.isHostObject(runtime)) { + void* data = object.getHostObject(runtime)->pointer(); + if (data != nullptr) { + std::memcpy(target, data, size); + } + return; + } + + const uint8_t* bytes = nullptr; + size_t byteLength = 0; + if (readJsiBuffer(runtime, object, &bytes, &byteLength)) { + if (bytes != nullptr) { + std::memcpy(target, bytes, std::min(byteLength, size)); + } + return; + } + } + + if (type.aggregateInfo == nullptr) { + throw facebook::jsi::JSError(runtime, "Missing native struct metadata."); + } + if (!value.isObject()) { + throw facebook::jsi::JSError(runtime, "Expected struct descriptor object."); + } + + Object object = value.asObject(runtime); + for (const auto& field : type.aggregateInfo->fields) { + bool hasField = object.hasProperty(runtime, field.name.c_str()); + if (!hasField) { + continue; + } + Value fieldValue = object.getProperty(runtime, field.name.c_str()); + void* fieldTarget = static_cast(target) + field.offset; + convertJsiArgument(runtime, bridge, field.type, fieldValue, fieldTarget, + frame); + } +} + +void convertIndexedAggregateArgument(Runtime& runtime, + const std::shared_ptr& bridge, + const NativeApiJsiType& type, + const Value& value, void* target, + NativeApiJsiArgumentFrame& frame) { + size_t size = nativeSizeForType(type); + std::memset(target, 0, size); + if (value.isNull() || value.isUndefined()) { + return; + } + if (value.isObject()) { + const uint8_t* bytes = nullptr; + size_t byteLength = 0; + if (readJsiBuffer(runtime, value.asObject(runtime), &bytes, &byteLength)) { + if (bytes != nullptr) { + std::memcpy(target, bytes, std::min(byteLength, size)); + } + return; + } + } + if (!value.isObject() || !value.asObject(runtime).isArray(runtime)) { + throw facebook::jsi::JSError(runtime, "Expected array, ArrayBuffer, or typed array."); + } + + Array array = value.asObject(runtime).getArray(runtime); + size_t elementSize = type.elementType != nullptr ? nativeSizeForType(*type.elementType) : 0; + if (elementSize == 0 || type.elementType == nullptr) { + throw facebook::jsi::JSError(runtime, "Invalid native array element type."); + } + size_t count = std::min(type.arraySize, array.size(runtime)); + for (size_t i = 0; i < count; i++) { + void* slot = static_cast(target) + (i * elementSize); + convertJsiArgument(runtime, bridge, *type.elementType, + array.getValueAtIndex(runtime, i), slot, frame); + } +} + +void convertJsiFfiArgument(Runtime& runtime, + const std::shared_ptr& bridge, + const NativeApiJsiType& type, const Value& value, + void* target, NativeApiJsiArgumentFrame& frame) { + if (type.kind != metagen::mdTypeArray) { + convertJsiArgument(runtime, bridge, type, value, target, frame); + return; + } + + void* pointer = nullptr; + if (!value.isNull() && !value.isUndefined()) { + if (value.isObject()) { + Object object = value.asObject(runtime); + if (!readPointerLikeValue(runtime, value, &pointer)) { + const uint8_t* bytes = nullptr; + size_t byteLength = 0; + if (readJsiBuffer(runtime, object, &bytes, &byteLength)) { + pointer = const_cast(bytes); + } + } + } + + if (pointer == nullptr) { + size_t byteLength = nativeSizeForType(type); + void* buffer = frame.addBuffer(byteLength); + convertIndexedAggregateArgument(runtime, bridge, type, value, buffer, + frame); + pointer = buffer; + } + } + + *static_cast(target) = pointer; +} + +void convertJsiArgument(Runtime& runtime, + const std::shared_ptr& bridge, + const NativeApiJsiType& type, + const Value& value, void* target, + NativeApiJsiArgumentFrame& frame) { + if (unsupportedJsiType(type)) { + throw facebook::jsi::JSError(runtime, + "This native signature is not supported by " + "the pure JSI bridge yet."); + } + + switch (type.kind) { + case metagen::mdTypeBool: + if (!value.isNumber() && !value.isBool()) { + throw facebook::jsi::JSError(runtime, + "Expected boolean or numeric argument."); + } + *static_cast(target) = + value.isBool() ? static_cast(value.getBool()) + : static_cast(value.getNumber() != 0); + break; + case metagen::mdTypeChar: + writeNumericArgument(runtime, value, target, "int8"); + break; + case metagen::mdTypeUChar: + case metagen::mdTypeUInt8: + writeNumericArgument(runtime, value, target, "uint8"); + break; + case metagen::mdTypeSShort: + writeNumericArgument(runtime, value, target, "int16"); + break; + case metagen::mdTypeUShort: + if (value.isString()) { + std::string text = value.asString(runtime).utf8(runtime); + if (text.size() != 1) { + throw facebook::jsi::JSError( + runtime, "Expected a single-character string."); + } + *static_cast(target) = + static_cast(static_cast(text[0])); + } else { + writeNumericArgument(runtime, value, target, "uint16"); + } + break; + case metagen::mdTypeSInt: + writeNumericArgument(runtime, value, target, "int32"); + break; + case metagen::mdTypeUInt: + writeNumericArgument(runtime, value, target, "uint32"); + break; + case metagen::mdTypeSLong: + case metagen::mdTypeSInt64: + writeNumericArgument(runtime, value, target, "int64"); + break; + case metagen::mdTypeULong: + case metagen::mdTypeUInt64: + writeNumericArgument(runtime, value, target, "uint64"); + break; + case metagen::mdTypeFloat: + writeNumericArgument(runtime, value, target, "float"); + break; + case metagen::mdTypeDouble: + writeNumericArgument(runtime, value, target, "double"); + break; + case metagen::mdTypeString: { + if (value.isNull() || value.isUndefined()) { + *static_cast(target) = nullptr; + break; + } + if (value.isObject()) { + Object object = value.asObject(runtime); + void* pointer = nullptr; + if (readPointerLikeValue(runtime, value, &pointer)) { + *static_cast(target) = static_cast(pointer); + break; + } + const uint8_t* bytes = nullptr; + size_t byteLength = 0; + if (readJsiBuffer(runtime, object, &bytes, &byteLength)) { + *static_cast(target) = + reinterpret_cast(const_cast(bytes)); + break; + } + Value valueOfValue = object.getProperty(runtime, "valueOf"); + if (valueOfValue.isObject() && + valueOfValue.asObject(runtime).isFunction(runtime)) { + Value primitive = valueOfValue.asObject(runtime) + .asFunction(runtime) + .callWithThis(runtime, object, nullptr, 0); + if (primitive.isString()) { + std::string utf8 = primitive.asString(runtime).utf8(runtime); + char* string = strdup(utf8.c_str()); + *static_cast(target) = string; + break; + } + } + } + if (!value.isString()) { + throw facebook::jsi::JSError(runtime, "Expected string argument."); + } + std::string utf8 = value.asString(runtime).utf8(runtime); + char* string = strdup(utf8.c_str()); + *static_cast(target) = string; + break; + } + case metagen::mdTypeAnyObject: + case metagen::mdTypeProtocolObject: + case metagen::mdTypeClassObject: + case metagen::mdTypeInstanceObject: + case metagen::mdTypeNSStringObject: + case metagen::mdTypeNSMutableStringObject: { + id object = objectFromJsiValue( + runtime, bridge, value, frame, + type.kind == metagen::mdTypeNSMutableStringObject); + *static_cast(target) = object; + break; + } + case metagen::mdTypeClass: { + *static_cast(target) = classFromJsiValue(runtime, value); + break; + } + case metagen::mdTypeSelector: { + if (value.isNull() || value.isUndefined()) { + *static_cast(target) = nullptr; + break; + } + if (!value.isString()) { + throw facebook::jsi::JSError(runtime, "Expected selector string."); + } + std::string selectorName = value.asString(runtime).utf8(runtime); + *static_cast(target) = sel_registerName(selectorName.c_str()); + break; + } + case metagen::mdTypePointer: + if (value.isObject()) { + Object object = value.asObject(runtime); + if (object.isHostObject(runtime)) { + auto reference = object.getHostObject(runtime); + if (reference->data() == nullptr && type.elementType != nullptr) { + reference->ensureStorage(runtime, *type.elementType, frame); + } else if (reference->data() == nullptr) { + reference->ensureStorage(runtime, reference->type(), frame); + } + void* pointer = reference->data(); + frame.rememberRoundTripValue(bridge, runtime, pointer, value); + *static_cast(target) = pointer; + break; + } + if (object.isHostObject(runtime)) { + void* pointer = + object.getHostObject(runtime) + ->data(); + frame.rememberRoundTripValue(bridge, runtime, pointer, value); + *static_cast(target) = pointer; + break; + } + const uint8_t* bytes = nullptr; + size_t byteLength = 0; + if (readJsiBuffer(runtime, object, &bytes, &byteLength)) { + void* pointer = const_cast(bytes); + frame.rememberRoundTripValue(bridge, runtime, pointer, value); + *static_cast(target) = pointer; + break; + } + } + *static_cast(target) = pointerFromJsiValue(runtime, value, frame); + break; + case metagen::mdTypeOpaquePointer: + *static_cast(target) = pointerFromJsiValue(runtime, value, frame); + break; + case metagen::mdTypeBlock: + case metagen::mdTypeFunctionPointer: { + if (value.isObject()) { + Object object = value.asObject(runtime); + void* nativePointer = nullptr; + if (readNativePointerProperty(runtime, object, &nativePointer)) { + *static_cast(target) = nativePointer; + break; + } + if (object.isFunction(runtime)) { + auto callback = createJsiCallback( + runtime, bridge, type, object.asFunction(runtime), + type.kind == metagen::mdTypeBlock); + void* pointer = callback->functionPointer(); + if (type.kind == metagen::mdTypeBlock) { + frame.addLifetime(callback); + frame.rememberRoundTripValue(bridge, runtime, pointer, value); + } else { + bridge->rememberRoundTripValue(runtime, pointer, value); + } + try { + object.setProperty(runtime, "__nativeApiPointerObject", + createPointer(runtime, bridge, pointer)); + object.setProperty( + runtime, "__nativeApiPointer", + static_cast(reinterpret_cast(pointer))); + } catch (const std::exception&) { + } + *static_cast(target) = pointer; + break; + } + } + *static_cast(target) = pointerFromJsiValue(runtime, value, frame); + break; + } + case metagen::mdTypeStruct: + convertAggregateArgument(runtime, bridge, type, value, target, frame); + break; + case metagen::mdTypeArray: + case metagen::mdTypeVector: + case metagen::mdTypeExtVector: + case metagen::mdTypeComplex: + convertIndexedAggregateArgument(runtime, bridge, type, value, target, + frame); + break; + default: + throw facebook::jsi::JSError(runtime, "Unsupported JSI argument type."); + } +} + +Value convertNativeReturnValue(Runtime& runtime, + const std::shared_ptr& bridge, + const NativeApiJsiType& type, void* value) { + if (unsupportedJsiType(type)) { + throw facebook::jsi::JSError(runtime, + "This native return type is not supported by " + "the pure JSI bridge yet."); + } + + switch (type.kind) { + case metagen::mdTypeVoid: + return Value::undefined(); + case metagen::mdTypeBool: + return *static_cast(value) != 0; + case metagen::mdTypeChar: + return static_cast(*static_cast(value)); + case metagen::mdTypeUChar: + case metagen::mdTypeUInt8: + return static_cast(*static_cast(value)); + case metagen::mdTypeSShort: + return static_cast(*static_cast(value)); + case metagen::mdTypeUShort: { + uint16_t raw = *static_cast(value); + if (raw >= 32 && raw <= 126) { + char buffer[2] = {static_cast(raw), '\0'}; + return String::createFromUtf8(runtime, buffer); + } + return static_cast(raw); + } + case metagen::mdTypeSInt: + return static_cast(*static_cast(value)); + case metagen::mdTypeUInt: + return static_cast(*static_cast(value)); + case metagen::mdTypeSLong: + case metagen::mdTypeSInt64: + return signedInteger64ToJsiValue(runtime, *static_cast(value)); + case metagen::mdTypeULong: + case metagen::mdTypeUInt64: + return unsignedInteger64ToJsiValue(runtime, + *static_cast(value)); + case metagen::mdTypeFloat: + return static_cast(*static_cast(value)); + case metagen::mdTypeDouble: + return *static_cast(value); + case metagen::mdTypeString: { + const char* string = *static_cast(value); + if (string == nullptr) { + return Value::null(); + } + NativeApiJsiType cStringType = + primitiveInteropType(metagen::mdTypeChar); + return Object::createFromHostObject( + runtime, std::make_shared( + bridge, cStringType, const_cast(string), false)); + } + case metagen::mdTypeClass: { + Class cls = *static_cast(value); + if (cls == nil) { + return Value::null(); + } + const char* name = class_getName(cls); + NativeApiSymbol symbol{ + .kind = NativeApiSymbolKind::Class, + .offset = MD_SECTION_OFFSET_NULL, + .name = name != nullptr ? name : "", + .runtimeName = name != nullptr ? name : "", + }; + if (const NativeApiSymbol* found = bridge->findClass(symbol.name)) { + symbol = *found; + } + return makeNativeClassValue(runtime, bridge, std::move(symbol)); + } + case metagen::mdTypeAnyObject: + case metagen::mdTypeProtocolObject: + case metagen::mdTypeClassObject: + case metagen::mdTypeInstanceObject: + case metagen::mdTypeNSStringObject: + case metagen::mdTypeNSMutableStringObject: { + id object = *static_cast(value); + if (object == nil) { + return Value::null(); + } + if ([object isKindOfClass:[NSNull class]]) { + if (type.returnOwned) { + [object release]; + } + return Value::null(); + } + if ([object respondsToSelector:@selector(UTF8String)]) { + bool untypedObject = type.kind == metagen::mdTypeAnyObject; + bool explicitNSString = type.kind == metagen::mdTypeNSStringObject; + if (untypedObject || explicitNSString) { + std::string utf8 = utf8StringFromNSString(static_cast(object)); + if (type.returnOwned) { + [object release]; + } + return makeString(runtime, utf8); + } + } + if ([object isKindOfClass:[NSNumber class]] && + ![object isKindOfClass:[NSDecimalNumber class]]) { + NSNumber* number = static_cast(object); + const char* objCType = [number objCType]; + bool isBool = CFGetTypeID((__bridge CFTypeRef)number) == + CFBooleanGetTypeID() || + (objCType != nullptr && + std::strcmp(objCType, @encode(BOOL)) == 0); + Value result = isBool ? Value(static_cast([number boolValue])) + : Value([number doubleValue]); + if (type.returnOwned) { + [object release]; + } + return result; + } + Value roundTrip = bridge->findRoundTripValue(runtime, object); + if (!roundTrip.isUndefined()) { + if (type.returnOwned) { + [object release]; + } + return roundTrip; + } + if (const NativeApiSymbol* classSymbol = + bridge->findClassForRuntimePointer((void*)object)) { + return makeNativeClassValue(runtime, bridge, *classSymbol); + } + if (const NativeApiSymbol* protocolSymbol = + bridge->findProtocolForRuntimePointer((void*)object)) { + return makeNativeProtocolValue(runtime, bridge, *protocolSymbol); + } + return makeNativeObjectValue(runtime, bridge, object, type.returnOwned); + } + case metagen::mdTypeSelector: { + SEL selector = *static_cast(value); + const char* selectorName = selector != nullptr ? sel_getName(selector) : nullptr; + return selectorName != nullptr ? makeString(runtime, selectorName) + : Value::null(); + } + case metagen::mdTypePointer: + case metagen::mdTypeOpaquePointer: { + void* pointer = *static_cast(value); + if (pointer == nullptr) { + return Value::null(); + } + if (const NativeApiSymbol* classSymbol = + bridge->findClassForRuntimePointer(pointer)) { + return makeNativeClassValue(runtime, bridge, *classSymbol); + } + if (const NativeApiSymbol* protocolSymbol = + bridge->findProtocolForRuntimePointer(pointer)) { + return makeNativeProtocolValue(runtime, bridge, *protocolSymbol); + } + if (type.kind == metagen::mdTypePointer && type.elementType != nullptr) { + std::shared_ptr backingValue; + Value roundTrip = bridge->findRoundTripValue(runtime, pointer); + if (!roundTrip.isUndefined()) { + backingValue = std::make_shared(runtime, roundTrip); + } + return Object::createFromHostObject( + runtime, std::make_shared( + bridge, *type.elementType, pointer, false, 0, nullptr, + std::move(backingValue))); + } + return createPointer(runtime, bridge, pointer); + } + case metagen::mdTypeBlock: + case metagen::mdTypeFunctionPointer: { + void* pointer = *static_cast(value); + if (pointer == nullptr) { + return Value::null(); + } + Value roundTrip = bridge->findRoundTripValue(runtime, pointer); + if (!roundTrip.isUndefined()) { + return roundTrip; + } + return wrapNativeFunctionPointer(runtime, bridge, type, pointer, + type.kind == metagen::mdTypeBlock); + } + case metagen::mdTypeStruct: + if (type.aggregateInfo == nullptr) { + return ArrayBuffer( + runtime, std::make_shared( + value, nativeSizeForType(type))); + } + return Object::createFromHostObject( + runtime, std::make_shared( + bridge, type.aggregateInfo, value, true)); + case metagen::mdTypeArray: + case metagen::mdTypeVector: + case metagen::mdTypeExtVector: + case metagen::mdTypeComplex: { + Array result(runtime, type.arraySize); + if (type.elementType == nullptr) { + return result; + } + size_t elementSize = nativeSizeForType(*type.elementType); + auto base = static_cast(value); + for (uint16_t i = 0; i < type.arraySize; i++) { + result.setValueAtIndex( + runtime, i, + convertNativeReturnValue(runtime, bridge, *type.elementType, + base + (static_cast(i) * elementSize))); + } + return result; + } + default: + throw facebook::jsi::JSError(runtime, "Unsupported JSI return type."); + } +} + +void NativeApiReferenceHostObject::ensureStorage( + Runtime& runtime, NativeApiJsiType type, NativeApiJsiArgumentFrame& frame, + size_t elements) { + size_t elementCount = std::max(elements, 1); + NativeApiJsiType storageType = std::move(type); + size_t stride = std::max(nativeSizeForType(storageType), 1); + size_t required = std::max(stride * elementCount, sizeof(void*)); + type_ = std::move(storageType); + + if (data_ == nullptr) { + data_ = calloc(1, required); + ownsData_ = true; + byteLength_ = required; + } else if (ownsData_ && byteLength_ < required) { + void* expanded = realloc(data_, required); + if (expanded == nullptr) { + throw std::bad_alloc(); + } + std::memset(static_cast(expanded) + byteLength_, 0, + required - byteLength_); + data_ = expanded; + byteLength_ = required; + } + + if (data_ != nullptr && pendingValue_ != nullptr) { + Value pending(runtime, *pendingValue_); + convertJsiArgument(runtime, bridge_, type_, pending, data_, frame); + pendingValue_.reset(); + } +} + +Value NativeApiReferenceHostObject::get(Runtime& runtime, + const PropNameID& name) { + std::string property = name.utf8(runtime); + if (property == "kind") { + return makeString(runtime, "reference"); + } + if (property == "address") { + return static_cast(reinterpret_cast(data_)); + } + if (property == "value") { + if (data_ == nullptr) { + if (pendingValue_ != nullptr) { + return Value(runtime, *pendingValue_); + } + return Value::undefined(); + } + return convertNativeReturnValue(runtime, bridge_, type_, data_); + } + if (auto index = parseArrayIndexProperty(property)) { + if (data_ == nullptr) { + return Value::undefined(); + } + void* slot = static_cast(data_) + + (*index * referenceElementStride(type_)); + return convertNativeReturnValue(runtime, bridge_, type_, slot); + } + if (property == "toString") { + void* data = data_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "toString"), 0, + [data](Runtime& runtime, const Value&, const Value*, size_t) -> Value { + char address[32] = {}; + snprintf(address, sizeof(address), "%p", data); + return makeString(runtime, + ""); + }); + } + return Value::undefined(); +} + +void NativeApiReferenceHostObject::set(Runtime& runtime, + const PropNameID& name, + const Value& value) { + std::string property = name.utf8(runtime); + auto index = parseArrayIndexProperty(property); + if (property != "value" && !index) { + return; + } + size_t slotIndex = index.value_or(0); + NativeApiJsiArgumentFrame frame(1); + if (data_ == nullptr) { + if (slotIndex == 0) { + pendingValue_ = std::make_shared(runtime, value); + return; + } + ensureStorage(runtime, type_, frame, slotIndex + 1); + } + pendingValue_.reset(); + void* slot = static_cast(data_) + + (slotIndex * referenceElementStride(type_)); + convertJsiArgument(runtime, bridge_, type_, value, slot, frame); +} + +Value NativeApiStructObjectHostObject::get(Runtime& runtime, + const PropNameID& name) { + std::string property = name.utf8(runtime); + if (property == "kind") { + return makeString(runtime, info_ != nullptr && info_->isUnion ? "union" : "struct"); + } + if (property == "name") { + return makeString(runtime, info_ != nullptr ? info_->name : ""); + } + if (property == "sizeof") { + return static_cast(info_ != nullptr ? info_->size : 0); + } + if (property == "address") { + return static_cast(reinterpret_cast(data_)); + } + if (property == "toString") { + auto info = info_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "toString"), 0, + [info](Runtime& runtime, const Value&, const Value*, size_t) -> Value { + return makeString(runtime, + std::string("[NativeApiJsi ") + + (info != nullptr && info->isUnion ? "Union " : "Struct ") + + (info != nullptr ? info->name : "") + "]"); + }); + } + + if (info_ != nullptr && data_ != nullptr) { + for (const auto& field : info_->fields) { + if (field.name != property) { + continue; + } + void* fieldData = static_cast(data_) + field.offset; + if (field.type.kind == metagen::mdTypeStruct && + field.type.aggregateInfo != nullptr) { + return Object::createFromHostObject( + runtime, std::make_shared( + bridge_, field.type.aggregateInfo, fieldData, false, + ownedData_, backingValue_)); + } + return convertNativeReturnValue(runtime, bridge_, field.type, fieldData); + } + } + return Value::undefined(); +} + +void NativeApiStructObjectHostObject::set(Runtime& runtime, + const PropNameID& name, + const Value& value) { + std::string property = name.utf8(runtime); + if (info_ == nullptr || data_ == nullptr) { + throw facebook::jsi::JSError(runtime, "Struct is not initialized."); + } + for (const auto& field : info_->fields) { + if (field.name != property) { + continue; + } + NativeApiJsiArgumentFrame frame(1); + convertJsiArgument(runtime, bridge_, field.type, value, + static_cast(data_) + field.offset, frame); + return; + } + throw facebook::jsi::JSError(runtime, "No native struct field: " + property); +} + +std::vector NativeApiStructObjectHostObject::getPropertyNames( + Runtime& runtime) { + std::vector names; + addPropertyName(runtime, names, "kind"); + addPropertyName(runtime, names, "name"); + addPropertyName(runtime, names, "sizeof"); + addPropertyName(runtime, names, "address"); + addPropertyName(runtime, names, "toString"); + if (info_ != nullptr) { + for (const auto& field : info_->fields) { + addPropertyName(runtime, names, field.name.c_str()); + } + } + return names; +} + +NativeApiJsiType primitiveInteropType(MDTypeKind kind) { + NativeApiJsiType type; + type.kind = kind; + type.ffiType = ffiTypeForJsiKind(kind); + type.supported = type.ffiType != nullptr; + return type; +} + +std::optional primitiveInteropTypeFromCode(int32_t code) { + MDTypeKind kind = static_cast(code); + switch (kind) { + case metagen::mdTypeVoid: + case metagen::mdTypeBool: + case metagen::mdTypeChar: + case metagen::mdTypeUChar: + case metagen::mdTypeUInt8: + case metagen::mdTypeSShort: + case metagen::mdTypeUShort: + case metagen::mdTypeSInt: + case metagen::mdTypeUInt: + case metagen::mdTypeSLong: + case metagen::mdTypeULong: + case metagen::mdTypeSInt64: + case metagen::mdTypeUInt64: + case metagen::mdTypeFloat: + case metagen::mdTypeDouble: + case metagen::mdTypeString: + case metagen::mdTypeAnyObject: + case metagen::mdTypeProtocolObject: + case metagen::mdTypeClass: + case metagen::mdTypeSelector: + case metagen::mdTypePointer: + case metagen::mdTypeOpaquePointer: + case metagen::mdTypeBlock: + case metagen::mdTypeFunctionPointer: + return primitiveInteropType(kind); + default: + return std::nullopt; + } +} + +std::optional interopTypeFromValue( + Runtime& runtime, const std::shared_ptr& bridge, + const Value& value) { + if (value.isNumber()) { + return primitiveInteropTypeFromCode(static_cast(value.getNumber())); + } + + if (!value.isObject()) { + return std::nullopt; + } + + Object object = value.asObject(runtime); + Value typeCodeValue = object.getProperty(runtime, "__nativeApiTypeCode"); + if (typeCodeValue.isNumber()) { + return primitiveInteropTypeFromCode( + static_cast(typeCodeValue.getNumber())); + } + Value valueOfValue = object.getProperty(runtime, "valueOf"); + if (valueOfValue.isObject() && + valueOfValue.asObject(runtime).isFunction(runtime)) { + Value primitive = + valueOfValue.asObject(runtime).asFunction(runtime).callWithThis( + runtime, object, nullptr, 0); + if (primitive.isNumber()) { + return primitiveInteropTypeFromCode( + static_cast(primitive.getNumber())); + } + } + + Class descriptorClass = nativeClassFromJsiObject(runtime, object); + if (descriptorClass == Nil && + stringPropertyOrEmpty(runtime, object, "kind") == "class") { + descriptorClass = + static_cast(pointerFromSymbolLikeObject(runtime, object)); + } + if (descriptorClass != Nil) { + return nativeObjectReturnTypeForClass(descriptorClass); + } + + if (object.isHostObject(runtime)) { + auto structObject = object.getHostObject(runtime); + NativeApiJsiType type; + type.kind = metagen::mdTypeStruct; + type.aggregateInfo = structObject->info(); + type.aggregateOffset = type.aggregateInfo != nullptr + ? type.aggregateInfo->offset + : MD_SECTION_OFFSET_NULL; + type.aggregateIsUnion = type.aggregateInfo != nullptr && + type.aggregateInfo->isUnion; + type.ffiType = type.aggregateInfo != nullptr && type.aggregateInfo->ffi != nullptr + ? &type.aggregateInfo->ffi->type + : nullptr; + type.supported = type.ffiType != nullptr; + return type; + } + + Value kindValue = object.getProperty(runtime, "kind"); + if (kindValue.isString()) { + std::string kindName = kindValue.asString(runtime).utf8(runtime); + if (kindName == "pointer") { + return primitiveInteropType(metagen::mdTypePointer); + } + if (kindName == "reference") { + return primitiveInteropType(metagen::mdTypePointer); + } + if (kindName == "class") { + return nativeObjectReturnType(metagen::mdTypeInstanceObject); + } + if (kindName == "selector") { + return primitiveInteropType(metagen::mdTypeSelector); + } + if (kindName == "protocol") { + return primitiveInteropType(metagen::mdTypeProtocolObject); + } + if (kindName == "block") { + return primitiveInteropType(metagen::mdTypeBlock); + } + if (kindName == "functionPointer") { + return primitiveInteropType(metagen::mdTypeFunctionPointer); + } + if (kindName == "functionReference") { + return primitiveInteropType(metagen::mdTypeFunctionPointer); + } + } + Value offsetValue = object.getProperty(runtime, "metadataOffset"); + if (kindValue.isString() && offsetValue.isNumber()) { + std::string kindName = kindValue.asString(runtime).utf8(runtime); + if (kindName == "struct" || kindName == "union") { + bool isUnion = kindName == "union"; + auto info = bridge->aggregateInfoFor( + static_cast(offsetValue.getNumber()), isUnion); + NativeApiJsiType type; + type.kind = metagen::mdTypeStruct; + type.aggregateInfo = info; + type.aggregateOffset = info != nullptr ? info->offset : MD_SECTION_OFFSET_NULL; + type.aggregateIsUnion = isUnion; + type.ffiType = info != nullptr && info->ffi != nullptr ? &info->ffi->type : nullptr; + type.supported = type.ffiType != nullptr; + return type; + } + } + + return std::nullopt; +} + +Value makeAggregateConstructor(Runtime& runtime, + const std::shared_ptr& bridge, + const NativeApiSymbol& symbol) { + auto info = bridge->aggregateInfoFor(symbol); + auto constructor = Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, symbol.name.c_str()), 1, + [bridge, symbol, info](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + if (info == nullptr) { + throw facebook::jsi::JSError(runtime, + "Native aggregate metadata is unavailable: " + + symbol.name); + } + + NativeApiJsiType type; + type.kind = metagen::mdTypeStruct; + type.aggregateInfo = info; + type.aggregateOffset = info->offset; + type.aggregateIsUnion = info->isUnion; + type.ffiType = info->ffi != nullptr ? &info->ffi->type : nullptr; + type.supported = type.ffiType != nullptr; + + if (count > 0 && args[0].isObject()) { + void* pointer = nullptr; + if (readPointerLikeValue(runtime, args[0], &pointer) && pointer != nullptr) { + return Object::createFromHostObject( + runtime, std::make_shared( + bridge, info, pointer, false, nullptr, + std::make_shared(runtime, args[0]))); + } + } + + std::vector storage(info->size, 0); + if (count > 0) { + NativeApiJsiArgumentFrame frame(1); + convertAggregateArgument(runtime, bridge, type, args[0], + storage.data(), frame); + } + return Object::createFromHostObject( + runtime, std::make_shared( + bridge, info, storage.data(), true)); + }); + + constructor.setProperty(runtime, "kind", + makeString(runtime, symbol.kind == NativeApiSymbolKind::Union + ? "union" + : "struct")); + constructor.setProperty(runtime, "runtimeName", makeString(runtime, symbol.runtimeName)); + constructor.setProperty(runtime, "metadataOffset", static_cast(symbol.offset)); + constructor.setProperty(runtime, "sizeof", + static_cast(info != nullptr ? info->size : 0)); + constructor.setProperty( + runtime, "equals", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "equals"), 2, + [bridge, info](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + if (info == nullptr || count < 2) { + return false; + } + + NativeApiJsiType type; + type.kind = metagen::mdTypeStruct; + type.aggregateInfo = info; + type.aggregateOffset = info->offset; + type.aggregateIsUnion = info->isUnion; + type.ffiType = info->ffi != nullptr ? &info->ffi->type : nullptr; + type.supported = type.ffiType != nullptr; + + std::vector left(info->size, 0); + std::vector right(info->size, 0); + try { + NativeApiJsiArgumentFrame leftFrame(1); + convertAggregateArgument(runtime, bridge, type, args[0], + left.data(), leftFrame); + NativeApiJsiArgumentFrame rightFrame(1); + convertAggregateArgument(runtime, bridge, type, args[1], + right.data(), rightFrame); + } catch (const std::exception&) { + return false; + } + + return std::memcmp(left.data(), right.data(), info->size) == 0; + })); + Array fields(runtime, info != nullptr ? info->fields.size() : 0); + if (info != nullptr) { + for (size_t i = 0; i < info->fields.size(); i++) { + fields.setValueAtIndex(runtime, i, makeString(runtime, info->fields[i].name)); + } + } + constructor.setProperty(runtime, "fields", fields); + return constructor; +} + +size_t sizeofInteropType(Runtime& runtime, + const std::shared_ptr& bridge, + const Value& value) { + if (auto type = interopTypeFromValue(runtime, bridge, value)) { + return nativeSizeForType(*type); + } + + if (value.isObject()) { + Object object = value.asObject(runtime); + if (object.isHostObject(runtime) || + object.isHostObject(runtime) || + object.isHostObject(runtime) || + nativeClassFromJsiObject(runtime, object) != Nil) { + return sizeof(void*); + } + void* nativePointer = nullptr; + if (readNativePointerProperty(runtime, object, &nativePointer)) { + return sizeof(void*); + } + Value sizeValue = object.getProperty(runtime, "sizeof"); + if (sizeValue.isNumber()) { + return static_cast(sizeValue.getNumber()); + } + } + + throw facebook::jsi::JSError(runtime, "Invalid type for interop.sizeof."); +} + +Object createPointer(Runtime& runtime, + const std::shared_ptr& bridge, + void* pointer, bool adopted) { + if (!adopted && bridge != nullptr) { + Value cached = bridge->findPointerValue(runtime, pointer); + if (cached.isObject()) { + return cached.asObject(runtime); + } + } + + Object result = Object::createFromHostObject( + runtime, + std::make_shared(bridge, pointer, "pointer", + adopted)); + if (!adopted && bridge != nullptr) { + bridge->rememberPointerValue(runtime, pointer, Value(runtime, result)); + } + return result; +} + +void installInteropHasInstance(Runtime& runtime, Function& constructor, + const char* kind) { + Value symbolCtorValue = runtime.global().getProperty(runtime, "Symbol"); + if (!symbolCtorValue.isObject()) { + return; + } + + Object symbolCtor = symbolCtorValue.asObject(runtime); + Value hasInstanceValue = symbolCtor.getProperty(runtime, "hasInstance"); + if (!hasInstanceValue.isSymbol()) { + return; + } + + try { + Object objectCtor = runtime.global().getPropertyAsObject(runtime, "Object"); + Function defineProperty = + objectCtor.getPropertyAsFunction(runtime, "defineProperty"); + Object descriptor(runtime); + descriptor.setProperty(runtime, "configurable", true); + descriptor.setProperty( + runtime, "value", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "Symbol.hasInstance"), 1, + [kind = std::string(kind)](Runtime& runtime, const Value&, + const Value* args, size_t count) -> Value { + if (count < 1 || !args[0].isObject()) { + return false; + } + + Object object = args[0].asObject(runtime); + Value kindValue = object.getProperty(runtime, "kind"); + return kindValue.isString() && + kindValue.asString(runtime).utf8(runtime) == kind; + })); + defineProperty.call(runtime, constructor, hasInstanceValue, descriptor); + } catch (const std::exception&) { + } +} + +Class classFromJsiValue(Runtime& runtime, const Value& value) { + if (value.isString()) { + std::string name = value.asString(runtime).utf8(runtime); + return objc_lookUpClass(name.c_str()); + } + if (!value.isObject()) { + return Nil; + } + Object object = value.asObject(runtime); + if (Class cls = nativeClassFromJsiObject(runtime, object)) { + return cls; + } + if (stringPropertyOrEmpty(runtime, object, "kind") == "class") { + if (void* pointer = pointerFromSymbolLikeObject(runtime, object)) { + return static_cast(pointer); + } + } + if (object.isHostObject(runtime)) { + id nativeObject = object.getHostObject(runtime)->object(); + return nativeObject != nil ? object_getClass(nativeObject) : Nil; + } + return Nil; +} + +Protocol* protocolFromJsiValue(Runtime& runtime, const Value& value) { + if (value.isString()) { + std::string name = value.asString(runtime).utf8(runtime); + Protocol* protocol = objc_getProtocol(name.c_str()); + if (protocol == nullptr) { + constexpr const char* suffix = "Protocol"; + if (name.size() > std::strlen(suffix) && + name.compare(name.size() - std::strlen(suffix), std::strlen(suffix), + suffix) == 0) { + protocol = objc_getProtocol( + name.substr(0, name.size() - std::strlen(suffix)).c_str()); + } + } + return protocol; + } + if (!value.isObject()) { + return nullptr; + } + Object object = value.asObject(runtime); + if (object.isHostObject(runtime)) { + return object.getHostObject(runtime) + ->nativeProtocol(); + } + if (stringPropertyOrEmpty(runtime, object, "kind") == "protocol") { + return static_cast(pointerFromSymbolLikeObject(runtime, object)); + } + if (object.isHostObject(runtime)) { + return static_cast( + object.getHostObject(runtime)->pointer()); + } + void* nativePointer = nullptr; + if (readNativePointerProperty(runtime, object, &nativePointer)) { + return static_cast(nativePointer); + } + Value nameValue = object.getProperty(runtime, "name"); + if (nameValue.isString()) { + return protocolFromJsiValue(runtime, nameValue); + } + return nullptr; +} + +Object createInteropObject(Runtime& runtime, + const std::shared_ptr& bridge) { + Object interop(runtime); + Object types(runtime); + auto setType = [&](const char* name, MDTypeKind kind) { + Object type(runtime); + double code = static_cast(kind); + type.setProperty(runtime, "__nativeApiTypeCode", code); + type.setProperty( + runtime, "valueOf", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "valueOf"), 0, + [code](Runtime&, const Value&, const Value*, size_t) -> Value { + return code; + })); + type.setProperty( + runtime, "toString", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "toString"), 0, + [code](Runtime& runtime, const Value&, const Value*, size_t) -> Value { + char text[32] = {}; + snprintf(text, sizeof(text), "%d", static_cast(code)); + return makeString(runtime, text); + })); + types.setProperty(runtime, name, type); + }; + setType("void", metagen::mdTypeVoid); + setType("bool", metagen::mdTypeBool); + setType("int8", metagen::mdTypeChar); + setType("uint8", metagen::mdTypeUInt8); + setType("int16", metagen::mdTypeSShort); + setType("uint16", metagen::mdTypeUShort); + setType("int32", metagen::mdTypeSInt); + setType("uint32", metagen::mdTypeUInt); + setType("int64", metagen::mdTypeSInt64); + setType("uint64", metagen::mdTypeUInt64); + setType("float", metagen::mdTypeFloat); + setType("double", metagen::mdTypeDouble); + setType("UTF8CString", metagen::mdTypeString); + setType("unichar", metagen::mdTypeUShort); + setType("id", metagen::mdTypeAnyObject); + setType("class", metagen::mdTypeClass); + setType("protocol", metagen::mdTypeProtocolObject); + setType("SEL", metagen::mdTypeSelector); + setType("selector", metagen::mdTypeSelector); + setType("pointer", metagen::mdTypePointer); + setType("block", metagen::mdTypeBlock); + setType("functionPointer", metagen::mdTypeFunctionPointer); + interop.setProperty(runtime, "types", types); + + Function pointerConstructor = Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "Pointer"), 1, + [bridge](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + if (count > 0 && args[0].isObject()) { + Object object = args[0].asObject(runtime); + if (object.isHostObject(runtime)) { + return Value(runtime, object); + } + } + void* pointer = nullptr; + if (count > 0 && !args[0].isNull() && !args[0].isUndefined()) { + auto readAddress = [&](const Value& value, + uintptr_t* address) -> bool { + auto readAddressFromString = [&](const Value& source) -> bool { + try { + Value stringCtorValue = + runtime.global().getProperty(runtime, "String"); + if (!stringCtorValue.isObject() || + !stringCtorValue.asObject(runtime).isFunction(runtime)) { + return false; + } + Value stringValue = + stringCtorValue.asObject(runtime).asFunction(runtime) + .call(runtime, source); + if (!stringValue.isString()) { + return false; + } + return parseIntegerTextToUintptr( + stringValue.asString(runtime).utf8(runtime), address); + } catch (const std::exception&) { + return false; + } + }; + + if (value.isNumber()) { + double number = value.getNumber(); + if (!std::isfinite(number)) { + return false; + } + *address = static_cast( + static_cast(number)); + return true; + } + if (value.isBigInt()) { + if (readAddressFromString(value)) { + return true; + } + BigInt bigint = value.getBigInt(runtime); + return parseBigIntToUintptr(runtime, bigint, address); + } + if (value.isObject()) { + Object object = value.asObject(runtime); + Value valueOfValue = object.getProperty(runtime, "valueOf"); + if (valueOfValue.isObject() && + valueOfValue.asObject(runtime).isFunction(runtime)) { + Value primitive = valueOfValue.asObject(runtime) + .asFunction(runtime) + .callWithThis(runtime, object, nullptr, 0); + if (primitive.isNumber()) { + double number = primitive.getNumber(); + if (!std::isfinite(number)) { + return false; + } + *address = static_cast( + static_cast(number)); + return true; + } + if (primitive.isBigInt()) { + if (readAddressFromString(primitive)) { + return true; + } + BigInt bigint = primitive.getBigInt(runtime); + return parseBigIntToUintptr(runtime, bigint, address); + } + } + return readAddressFromString(value); + } + return false; + }; + + uintptr_t address = 0; + if (!readAddress(args[0], &address)) { + throw facebook::jsi::JSError(runtime, + "Pointer expects a numeric address."); + } + pointer = reinterpret_cast(address); + } + return createPointer(runtime, bridge, pointer); + }); + Object pointerPrototype(runtime); + pointerPrototype.setProperty(runtime, "constructor", pointerConstructor); + pointerConstructor.setProperty(runtime, "prototype", pointerPrototype); + installInteropHasInstance(runtime, pointerConstructor, "pointer"); + pointerConstructor.setProperty(runtime, "kind", makeString(runtime, "pointer")); + pointerConstructor.setProperty(runtime, "sizeof", + static_cast(sizeof(void*))); + interop.setProperty(runtime, "Pointer", pointerConstructor); + + Function functionReferenceConstructor = Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "FunctionReference"), 1, + [](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + if (count < 1 || !args[0].isObject()) { + throw facebook::jsi::JSError( + runtime, "FunctionReference expects a function."); + } + + Object object = args[0].asObject(runtime); + if (!object.isFunction(runtime)) { + throw facebook::jsi::JSError( + runtime, "FunctionReference expects a function."); + } + + Function function = object.asFunction(runtime); + function.setProperty(runtime, "kind", + makeString(runtime, "functionReference")); + function.setProperty(runtime, "sizeof", + static_cast(sizeof(void*))); + return function; + }); + Object functionReferencePrototype(runtime); + functionReferencePrototype.setProperty(runtime, "constructor", + functionReferenceConstructor); + functionReferenceConstructor.setProperty(runtime, "prototype", + functionReferencePrototype); + installInteropHasInstance(runtime, functionReferenceConstructor, + "functionReference"); + functionReferenceConstructor.setProperty(runtime, "kind", + makeString(runtime, + "functionReference")); + functionReferenceConstructor.setProperty(runtime, "sizeof", + static_cast(sizeof(void*))); + interop.setProperty(runtime, "FunctionReference", + functionReferenceConstructor); + + Function referenceConstructor = Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "Reference"), 2, + [bridge](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + NativeApiJsiType type = primitiveInteropType(metagen::mdTypePointer); + bool firstArgumentIsType = false; + if (count > 1) { + firstArgumentIsType = true; + } else if (count == 1 && args[0].isObject()) { + Object object = args[0].asObject(runtime); + Value typeCodeValue = + object.getProperty(runtime, "__nativeApiTypeCode"); + Value kindValue = object.getProperty(runtime, "kind"); + firstArgumentIsType = + typeCodeValue.isNumber() || object.isFunction(runtime) || + nativeClassFromJsiObject(runtime, object) != Nil || + (kindValue.isString() && + (kindValue.asString(runtime).utf8(runtime) == "class" || + kindValue.asString(runtime).utf8(runtime) == "protocol")); + } + std::optional requestedType = + firstArgumentIsType + ? interopTypeFromValue(runtime, bridge, args[0]) + : std::nullopt; + bool hasType = firstArgumentIsType && requestedType.has_value(); + if (hasType) { + type = *requestedType; + } + + void* data = nullptr; + bool ownsData = false; + size_t byteLength = 0; + std::shared_ptr pendingValue; + if (hasType) { + bool usesExternalStorage = false; + Value valueToStore = Value::undefined(); + if (count > 1) { + valueToStore = Value(runtime, args[1]); + if (args[1].isObject()) { + Object object = args[1].asObject(runtime); + if (object.isHostObject(runtime)) { + data = object + .getHostObject( + runtime) + ->pointer(); + usesExternalStorage = true; + } else if (object.isHostObject( + runtime)) { + auto reference = + object.getHostObject( + runtime); + data = reference->data(); + if (data != nullptr) { + usesExternalStorage = true; + } else { + valueToStore = object.getProperty(runtime, "value"); + } + } else if (type.kind == metagen::mdTypeStruct && + object.isHostObject< + NativeApiStructObjectHostObject>(runtime)) { + data = object + .getHostObject< + NativeApiStructObjectHostObject>(runtime) + ->data(); + usesExternalStorage = true; + } else if (type.kind == metagen::mdTypePointer || + type.kind == metagen::mdTypeOpaquePointer || + type.kind == metagen::mdTypeBlock || + type.kind == metagen::mdTypeFunctionPointer) { + void* nativePointer = nullptr; + if (readNativePointerProperty(runtime, object, + &nativePointer)) { + data = nativePointer; + usesExternalStorage = true; + } + } + } + } + if (!usesExternalStorage) { + byteLength = std::max(nativeSizeForType(type), + sizeof(void*)); + data = calloc(1, byteLength); + if (data == nullptr) { + throw std::bad_alloc(); + } + ownsData = true; + if (count > 1) { + NativeApiJsiArgumentFrame frame(1); + convertJsiArgument(runtime, bridge, type, valueToStore, data, + frame); + } + } + } else if (count > 0) { + pendingValue = std::make_shared(runtime, args[0]); + } + + if (ownsData && data == nullptr) { + throw std::bad_alloc(); + } + return Object::createFromHostObject( + runtime, std::make_shared( + bridge, type, data, ownsData, byteLength, + std::move(pendingValue))); + }); + Object referencePrototype(runtime); + referencePrototype.setProperty(runtime, "constructor", referenceConstructor); + referenceConstructor.setProperty(runtime, "prototype", referencePrototype); + installInteropHasInstance(runtime, referenceConstructor, "reference"); + referenceConstructor.setProperty(runtime, "kind", + makeString(runtime, "reference")); + referenceConstructor.setProperty(runtime, "sizeof", + static_cast(sizeof(void*))); + interop.setProperty(runtime, "Reference", referenceConstructor); + + interop.setProperty( + runtime, "sizeof", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "sizeof"), 1, + [bridge](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + if (count < 1) { + throw facebook::jsi::JSError(runtime, "sizeof expects a type."); + } + return static_cast(sizeofInteropType(runtime, bridge, args[0])); + })); + + interop.setProperty( + runtime, "alloc", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "alloc"), 1, + [bridge](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + if (count < 1 || !args[0].isNumber()) { + throw facebook::jsi::JSError(runtime, "alloc expects a byte size."); + } + size_t size = static_cast(std::max(0, args[0].getNumber())); + return createPointer(runtime, bridge, calloc(1, size), false); + })); + + interop.setProperty( + runtime, "free", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "free"), 1, + [](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + if (count < 1 || !args[0].isObject()) { + return Value::undefined(); + } + Object object = args[0].asObject(runtime); + if (!object.isHostObject(runtime)) { + return Value::undefined(); + } + auto pointer = object.getHostObject(runtime); + void* raw = pointer->pointer(); + if (raw != nullptr) { + free(raw); + pointer->clearWithoutFree(); + } + return Value::undefined(); + })); + + interop.setProperty( + runtime, "adopt", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "adopt"), 1, + [](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + if (count < 1 || !args[0].isObject()) { + throw facebook::jsi::JSError(runtime, "adopt expects a Pointer."); + } + Object object = args[0].asObject(runtime); + if (!object.isHostObject(runtime)) { + throw facebook::jsi::JSError(runtime, "adopt expects a Pointer."); + } + object.getHostObject(runtime)->adopt(); + return Value(runtime, object); + })); + + interop.setProperty( + runtime, "handleof", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "handleof"), 1, + [bridge](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + if (count < 1 || args[0].isNull() || args[0].isUndefined()) { + return Value::null(); + } + if (args[0].isString()) { + std::string utf8 = args[0].asString(runtime).utf8(runtime); + char* data = strdup(utf8.c_str()); + return createPointer(runtime, bridge, data); + } + if (!args[0].isObject()) { + return Value::null(); + } + Object object = args[0].asObject(runtime); + if (object.isHostObject(runtime)) { + return Value(runtime, object); + } + if (object.isHostObject(runtime)) { + void* data = + object.getHostObject(runtime)->data(); + if (data == nullptr) { + throw facebook::jsi::JSError( + runtime, "Cannot get handle of empty Reference."); + } + return createPointer(runtime, bridge, data); + } + if (object.isHostObject(runtime)) { + auto structObject = + object.getHostObject(runtime); + if (structObject->backingValue() != nullptr) { + return Value(runtime, *structObject->backingValue()); + } + return createPointer(runtime, bridge, structObject->data()); + } + if (object.isHostObject(runtime)) { + return createPointer( + runtime, bridge, + object.getHostObject(runtime) + ->object()); + } + if (Class cls = nativeClassFromJsiObject(runtime, object)) { + return createPointer(runtime, bridge, cls); + } + if (object.isHostObject(runtime)) { + return createPointer( + runtime, bridge, + object.getHostObject(runtime) + ->nativeProtocol()); + } + if (void* symbolPointer = pointerFromSymbolLikeObject(runtime, object)) { + return createPointer(runtime, bridge, symbolPointer); + } + void* nativePointer = nullptr; + if (readNativePointerProperty(runtime, object, &nativePointer)) { + return createPointer(runtime, bridge, nativePointer); + } + Value kindValue = object.getProperty(runtime, "kind"); + if (kindValue.isString() && + kindValue.asString(runtime).utf8(runtime) == "functionReference") { + throw facebook::jsi::JSError( + runtime, "Cannot get handle of uninitialized FunctionReference."); + } + Value nativeName = object.getProperty(runtime, "nativeName"); + if (nativeName.isString()) { + std::string name = nativeName.asString(runtime).utf8(runtime); + void* symbol = dlsym(bridge->selfDl(), name.c_str()); + if (symbol != nullptr) { + return createPointer(runtime, bridge, symbol); + } + } + return Value::null(); + })); + + interop.setProperty( + runtime, "stringFromCString", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "stringFromCString"), 2, + [](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + if (count < 1 || args[0].isNull() || args[0].isUndefined()) { + return Value::null(); + } + NativeApiJsiArgumentFrame frame(1); + const char* data = + static_cast(pointerFromJsiValue(runtime, args[0], frame)); + if (data == nullptr) { + return Value::null(); + } + if (count > 1 && args[1].isNumber()) { + size_t length = static_cast(std::max(0, args[1].getNumber())); + return String::createFromUtf8(runtime, + reinterpret_cast(data), + length); + } + return makeString(runtime, data); + })); + + interop.setProperty( + runtime, "bufferFromData", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "bufferFromData"), 1, + [](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + if (count < 1 || !args[0].isObject()) { + throw facebook::jsi::JSError(runtime, "Invalid data."); + } + Object object = args[0].asObject(runtime); + if (object.isArrayBuffer(runtime)) { + return Value(runtime, object); + } + id native = nil; + if (object.isHostObject(runtime)) { + native = object.getHostObject(runtime)->object(); + } else if (object.isHostObject(runtime)) { + native = static_cast( + object.getHostObject(runtime)->pointer()); + } + if (native == nil || ![native isKindOfClass:[NSData class]]) { + throw facebook::jsi::JSError(runtime, "Invalid data."); + } + NSData* data = static_cast(native); + return ArrayBuffer( + runtime, std::make_shared( + data.bytes, static_cast(data.length))); + })); + + interop.setProperty( + runtime, "addMethod", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "addMethod"), 2, + [](Runtime& runtime, const Value&, const Value*, size_t) -> Value { + throw facebook::jsi::JSError( + runtime, + "interop.addMethod requires the JSI class builder layer."); + })); + interop.setProperty( + runtime, "addProtocol", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "addProtocol"), 2, + [](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + if (count < 2) { + throw facebook::jsi::JSError( + runtime, "interop.addProtocol expects class and protocol."); + } + Class cls = classFromJsiValue(runtime, args[0]); + Protocol* protocol = protocolFromJsiValue(runtime, args[1]); + if (cls == Nil || protocol == nullptr) { + return false; + } + return class_addProtocol(cls, protocol); + })); + + return interop; +} diff --git a/NativeScript/ffi/shared/jsi/NativeApiJsiHostObject.h b/NativeScript/ffi/shared/jsi/NativeApiJsiHostObject.h new file mode 100644 index 00000000..487f6fe9 --- /dev/null +++ b/NativeScript/ffi/shared/jsi/NativeApiJsiHostObject.h @@ -0,0 +1,539 @@ +#ifndef NATIVESCRIPT_NATIVE_API_HAS_ENGINE_LAZY_GLOBALS +inline bool InstallNativeApiEngineLazyGlobal( + Runtime&, std::shared_ptr, const std::string&, + const std::string&, bool) { + return false; +} +#endif + +class NativeApiHostObject final : public HostObject { + public: + explicit NativeApiHostObject(std::shared_ptr bridge) + : bridge_(std::move(bridge)) {} + + Value get(Runtime& runtime, const PropNameID& name) override { + std::string property = name.utf8(runtime); + if (property == "runtime") { + return makeString(runtime, "jsi"); + } + if (property == "backend") { + return makeString(runtime, "hermes"); + } + if (property == "metadata") { + return metadataObject(runtime); + } + if (property == "hasScheduler") { + return bridge_->scheduler() != nullptr; + } + if (property == "interop") { + return createInteropObject(runtime, bridge_); + } +#ifdef NATIVESCRIPT_NATIVE_API_HAS_ENGINE_LAZY_GLOBALS + if (property == "__defineLazyGlobal") { + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "__defineLazyGlobal"), 3, + [bridge](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + std::string name = readStringArg(runtime, args, count, 0, "name"); + std::string kind = readStringArg(runtime, args, count, 1, "kind"); + bool force = count > 2 && args[2].isBool() && args[2].getBool(); + return InstallNativeApiEngineLazyGlobal(runtime, bridge, name, kind, + force); + }); + } +#endif + if (property == "__fastEnumeration") { + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "__fastEnumeration"), 1, + [bridge](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + if (count < 1 || !args[0].isObject()) { + throw facebook::jsi::JSError( + runtime, "Fast enumeration expects a native object."); + } + id object = NativeApiObjectHostObject::nativeObjectFromValue(runtime, args[0]); + if (object == nil) { + throw facebook::jsi::JSError( + runtime, "Fast enumeration expects a native object."); + } + if (![object conformsToProtocol:@protocol(NSFastEnumeration)]) { + throw facebook::jsi::JSError( + runtime, "Object does not conform to NSFastEnumeration."); + } + return Object::createFromHostObject( + runtime, + std::make_shared( + bridge, static_cast>(object))); + }); + } + if (property == "runOnUI") { + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "runOnUI"), 1, + [bridge](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + auto scheduler = bridge->scheduler(); + if (scheduler == nullptr) { + throw facebook::jsi::JSError( + runtime, + "NativeApiJsi was installed without a UI scheduler."); + } + + std::shared_ptr callback; + if (count > 0 && !args[0].isNull() && !args[0].isUndefined()) { + if (!args[0].isObject()) { + throw facebook::jsi::JSError( + runtime, "runOnUI expects a function callback."); + } + + Object callbackObject = args[0].asObject(runtime); + if (!callbackObject.isFunction(runtime)) { + throw facebook::jsi::JSError( + runtime, "runOnUI expects a function callback."); + } + callback = std::make_shared( + callbackObject.asFunction(runtime)); + } + + Runtime* runtimePtr = &runtime; + auto promiseCtor = + runtime.global().getPropertyAsFunction(runtime, "Promise"); + return promiseCtor.callAsConstructor( + runtime, + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "runOnUIPromise"), + 2, + [scheduler, runtimePtr, callback]( + Runtime& promiseRuntime, const Value&, + const Value* promiseArgs, + size_t promiseArgc) -> Value { + if (promiseArgc < 2 || !promiseArgs[0].isObject() || + !promiseArgs[1].isObject()) { + return Value::undefined(); + } + + auto resolve = std::make_shared( + promiseArgs[0].asObject(promiseRuntime) + .asFunction(promiseRuntime)); + auto reject = std::make_shared( + promiseArgs[1].asObject(promiseRuntime) + .asFunction(promiseRuntime)); + if (callback == nullptr) { + scheduler->invokeOnUI([scheduler, runtimePtr, resolve]() { + scheduler->invokeOnJS([runtimePtr, resolve]() { + resolve->call(*runtimePtr); + }); + }); + return Value::undefined(); + } + + scheduler->invokeOnJS([runtimePtr, callback, resolve, reject]() { + try { + { + ScopedNativeApiUINativeCallDispatch uiDispatch; + callback->call(*runtimePtr); + } + resolve->call(*runtimePtr); + } catch (const std::exception& error) { + reject->call( + *runtimePtr, + String::createFromUtf8(*runtimePtr, error.what())); + } + }); + + return Value::undefined(); + })); + }); + } + if (property == "import") { + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "import"), 1, + [](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + std::string path = readStringArg(runtime, args, count, 0, "path"); + std::string frameworkPath = path; + if (!frameworkPath.empty() && frameworkPath[0] != '/') { + frameworkPath = "/System/Library/Frameworks/" + frameworkPath + + ".framework"; + } + + NSBundle* bundle = [NSBundle + bundleWithPath:[NSString stringWithUTF8String:frameworkPath.c_str()]]; + if (bundle == nil || ![bundle load]) { + throw facebook::jsi::JSError( + runtime, "Could not load bundle: " + frameworkPath); + } + return true; + }); + } + if (property == "lookup") { + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "lookup"), 1, + [bridge](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + std::string symbolName = + readStringArg(runtime, args, count, 0, "name"); + const NativeApiSymbol* symbol = bridge->find(symbolName); + if (symbol == nullptr) { + return Value::null(); + } + return symbolToObject(runtime, *symbol); + }); + } + if (property == "getClass") { + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "getClass"), 1, + [bridge](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + std::string className = + readStringArg(runtime, args, count, 0, "name"); + const NativeApiSymbol* symbol = bridge->findClass(className); + if (symbol == nullptr) { + Class cls = objc_lookUpClass(className.c_str()); + if (cls == nil) { + return Value::null(); + } + NativeApiSymbol runtimeSymbol{ + .kind = NativeApiSymbolKind::Class, + .offset = MD_SECTION_OFFSET_NULL, + .name = className, + .runtimeName = className, + }; + return makeNativeClassValue(runtime, bridge, + std::move(runtimeSymbol)); + } + + return makeNativeClassValue(runtime, bridge, *symbol); + }); + } + if (property == "__extendClass") { + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "__extendClass"), 2, + [bridge](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + return extendNativeApiJsiClass(runtime, bridge, args, count); + }); + } + if (property == "__invokeBase") { + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "__invokeBase"), 3, + [bridge](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + return invokeNativeApiJsiBaseMethod(runtime, bridge, args, count); + }); + } + if (property == "__rememberClassWrapper") { + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "__rememberClassWrapper"), 3, + [bridge](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + if (count < 2) { + return Value::undefined(); + } + Class cls = classFromJsiValue(runtime, args[0]); + if (cls == Nil) { + return Value::undefined(); + } + bridge->rememberClassValue(runtime, cls, args[1]); + if (count >= 3 && args[2].isObject()) { + bridge->rememberClassPrototype(runtime, cls, args[2]); + } + return Value::undefined(); + }); + } + if (property == "CC_SHA256") { + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "CC_SHA256"), 3, + [bridge](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + if (count < 3 || !args[1].isNumber()) { + throw facebook::jsi::JSError( + runtime, "CC_SHA256 expects data, length, and output."); + } + void* commonCrypto = + dlopen("/usr/lib/system/libcommonCrypto.dylib", + RTLD_NOW | RTLD_LOCAL); + void* symbol = commonCrypto != nullptr + ? dlsym(commonCrypto, "CC_SHA256") + : nullptr; + if (symbol == nullptr && commonCrypto != nullptr) { + symbol = dlsym(commonCrypto, "_CC_SHA256"); + } + if (symbol == nullptr) { + throw facebook::jsi::JSError(runtime, + "CC_SHA256 is not available."); + } + NativeApiJsiArgumentFrame frame(3); + void* data = pointerFromJsiValue(runtime, args[0], frame); + void* output = pointerFromJsiValue(runtime, args[2], frame); + using CC_SHA256_Fn = unsigned char* (*)(const void*, unsigned long, + unsigned char*); + auto fn = reinterpret_cast(symbol); + unsigned char* result = + fn(data, static_cast(args[1].getNumber()), + static_cast(output)); + return createPointer(runtime, bridge, result); + }); + } + if (property == "getFunction") { + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "getFunction"), 1, + [bridge](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + std::string functionName = + readStringArg(runtime, args, count, 0, "name"); + const NativeApiSymbol* symbol = bridge->findFunction(functionName); + if (symbol == nullptr) { + return Value::null(); + } + auto function = Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, symbol->name), 0, + [bridge, symbol = *symbol](Runtime& runtime, const Value&, + const Value* args, + size_t count) -> Value { + return callCFunction(runtime, bridge, symbol, args, count); + }); + function.setProperty(runtime, "kind", makeString(runtime, "function")); + function.setProperty(runtime, "nativeName", + makeString(runtime, symbol->name)); + function.setProperty(runtime, "metadataOffset", + static_cast(symbol->offset)); + function.setProperty(runtime, "sizeof", + static_cast(sizeof(void*))); + return function; + }); + } + if (property == "getConstant") { + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "getConstant"), 1, + [bridge](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + std::string constantName = + readStringArg(runtime, args, count, 0, "name"); + const NativeApiSymbol* symbol = bridge->findConstant(constantName); + if (symbol == nullptr) { + return Value::undefined(); + } + return constantToValue(runtime, bridge, *symbol); + }); + } + if (property == "getEnum") { + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "getEnum"), 1, + [bridge](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + std::string enumName = readStringArg(runtime, args, count, 0, "name"); + const NativeApiSymbol* symbol = bridge->findEnum(enumName); + if (symbol == nullptr) { + return Value::undefined(); + } + return enumToObject(runtime, bridge->metadata(), *symbol); + }); + } + if (property == "getProtocol") { + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "getProtocol"), 1, + [bridge](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + std::string protocolName = + readStringArg(runtime, args, count, 0, "name"); + const NativeApiSymbol* symbol = bridge->findProtocol(protocolName); + if (symbol == nullptr) { + Protocol* protocol = lookupProtocolByNativeName(protocolName); + if (protocol == nullptr) { + return Value::null(); + } + const char* runtimeName = protocol_getName(protocol); + NativeApiSymbol runtimeSymbol{ + .kind = NativeApiSymbolKind::Protocol, + .offset = MD_SECTION_OFFSET_NULL, + .name = protocolName, + .runtimeName = runtimeName != nullptr ? runtimeName : protocolName, + }; + return makeNativeProtocolValue(runtime, bridge, + std::move(runtimeSymbol)); + } + return makeNativeProtocolValue(runtime, bridge, *symbol); + }); + } + if (property == "getStruct" || property == "getUnion") { + auto bridge = bridge_; + bool isUnion = property == "getUnion"; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property.c_str()), 1, + [bridge, isUnion](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + std::string aggregateName = + readStringArg(runtime, args, count, 0, "name"); + const NativeApiSymbol* symbol = + isUnion ? bridge->findUnion(aggregateName) + : bridge->findStruct(aggregateName); + if (symbol == nullptr) { + return Value::undefined(); + } + return makeAggregateConstructor(runtime, bridge, *symbol); + }); + } + + if (const NativeApiSymbol* classSymbol = bridge_->findClass(property)) { + return makeNativeClassValue(runtime, bridge_, *classSymbol); + } + + if (const NativeApiSymbol* functionSymbol = bridge_->findFunction(property)) { + auto bridge = bridge_; + Function function = Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property.c_str()), 0, + [bridge, symbol = *functionSymbol](Runtime& runtime, const Value&, + const Value* args, + size_t count) -> Value { + return callCFunction(runtime, bridge, symbol, args, count); + }); + function.setProperty(runtime, "kind", makeString(runtime, "function")); + function.setProperty(runtime, "nativeName", + makeString(runtime, functionSymbol->name)); + function.setProperty(runtime, "metadataOffset", + static_cast(functionSymbol->offset)); + function.setProperty(runtime, "sizeof", + static_cast(sizeof(void*))); + return function; + } + + if (const NativeApiSymbol* constantSymbol = bridge_->findConstant(property)) { + return constantToValue(runtime, bridge_, *constantSymbol); + } + + if (const NativeApiSymbol* enumSymbol = bridge_->findEnum(property)) { + return enumToObject(runtime, bridge_->metadata(), *enumSymbol); + } + + if (const NativeApiSymbol* protocolSymbol = + bridge_->findProtocol(property)) { + return makeNativeProtocolValue(runtime, bridge_, *protocolSymbol); + } + + if (const NativeApiSymbol* aggregateSymbol = + bridge_->findAggregate(property)) { + return makeAggregateConstructor(runtime, bridge_, *aggregateSymbol); + } + + return Value::undefined(); + } + + std::vector getPropertyNames(Runtime& runtime) override { + std::vector names; + names.reserve(11); + addPropertyName(runtime, names, "runtime"); + addPropertyName(runtime, names, "backend"); + addPropertyName(runtime, names, "metadata"); + addPropertyName(runtime, names, "hasScheduler"); + addPropertyName(runtime, names, "interop"); +#ifdef NATIVESCRIPT_NATIVE_API_HAS_ENGINE_LAZY_GLOBALS + addPropertyName(runtime, names, "__defineLazyGlobal"); +#endif + addPropertyName(runtime, names, "runOnUI"); + addPropertyName(runtime, names, "import"); + addPropertyName(runtime, names, "lookup"); + addPropertyName(runtime, names, "getClass"); + addPropertyName(runtime, names, "__extendClass"); + addPropertyName(runtime, names, "__invokeBase"); + addPropertyName(runtime, names, "__rememberClassWrapper"); + addPropertyName(runtime, names, "getFunction"); + addPropertyName(runtime, names, "getConstant"); + addPropertyName(runtime, names, "getEnum"); + addPropertyName(runtime, names, "getProtocol"); + addPropertyName(runtime, names, "getStruct"); + addPropertyName(runtime, names, "getUnion"); + return names; + } + + private: + Object metadataObject(Runtime& runtime) const { + Object metadata(runtime); + metadata.setProperty(runtime, "classes", + static_cast(bridge_->classCount())); + metadata.setProperty(runtime, "functions", + static_cast(bridge_->functionCount())); + metadata.setProperty(runtime, "constants", + static_cast(bridge_->constantCount())); + metadata.setProperty(runtime, "protocols", + static_cast(bridge_->protocolCount())); + metadata.setProperty(runtime, "enums", + static_cast(bridge_->enumCount())); + metadata.setProperty(runtime, "structs", + static_cast(bridge_->structCount())); + metadata.setProperty(runtime, "unions", + static_cast(bridge_->unionCount())); + + metadata.setProperty( + runtime, "classNames", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "classNames"), 0, + [bridge = bridge_](Runtime& runtime, const Value&, const Value*, + size_t) -> Value { + return namesToArray(runtime, bridge->classNames()); + })); + metadata.setProperty( + runtime, "functionNames", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "functionNames"), 0, + [bridge = bridge_](Runtime& runtime, const Value&, const Value*, + size_t) -> Value { + return namesToArray(runtime, bridge->functionNames()); + })); + metadata.setProperty( + runtime, "constantNames", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "constantNames"), 0, + [bridge = bridge_](Runtime& runtime, const Value&, const Value*, + size_t) -> Value { + return namesToArray(runtime, bridge->constantNames()); + })); + metadata.setProperty( + runtime, "protocolNames", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "protocolNames"), 0, + [bridge = bridge_](Runtime& runtime, const Value&, const Value*, + size_t) -> Value { + return namesToArray(runtime, bridge->protocolNames()); + })); + metadata.setProperty( + runtime, "enumNames", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "enumNames"), 0, + [bridge = bridge_](Runtime& runtime, const Value&, const Value*, + size_t) -> Value { + return namesToArray(runtime, bridge->enumNames()); + })); + metadata.setProperty( + runtime, "structNames", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "structNames"), 0, + [bridge = bridge_](Runtime& runtime, const Value&, const Value*, + size_t) -> Value { + return namesToArray(runtime, bridge->structNames()); + })); + metadata.setProperty( + runtime, "unionNames", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "unionNames"), 0, + [bridge = bridge_](Runtime& runtime, const Value&, const Value*, + size_t) -> Value { + return namesToArray(runtime, bridge->unionNames()); + })); + return metadata; + } + + std::shared_ptr bridge_; +}; diff --git a/NativeScript/ffi/shared/jsi/NativeApiJsiHostObjects.h b/NativeScript/ffi/shared/jsi/NativeApiJsiHostObjects.h new file mode 100644 index 00000000..10472e16 --- /dev/null +++ b/NativeScript/ffi/shared/jsi/NativeApiJsiHostObjects.h @@ -0,0 +1,1759 @@ +class NativeApiPointerHostObject final + : public HostObject, + public std::enable_shared_from_this { + public: + NativeApiPointerHostObject(std::shared_ptr bridge, + void* pointer, std::string kind = "pointer", + bool adopted = false) + : bridge_(std::move(bridge)), + pointer_(pointer), + kind_(std::move(kind)), + adopted_(adopted) {} + + ~NativeApiPointerHostObject() override { + if (adopted_ && pointer_ != nullptr) { + if (bridge_ != nullptr) { + bridge_->forgetPointerValue(pointer_); + } + free(pointer_); + pointer_ = nullptr; + } + } + + void* pointer() const { return pointer_; } + bool adopted() const { return adopted_; } + void adopt() { adopted_ = true; } + void clearWithoutFree() { + if (bridge_ != nullptr) { + bridge_->forgetPointerValue(pointer_); + } + pointer_ = nullptr; + adopted_ = false; + } + + Value get(Runtime& runtime, const PropNameID& name) override { + std::string property = name.utf8(runtime); + if (property == "kind") { + return makeString(runtime, kind_); + } + if (property == "address") { + return static_cast(reinterpret_cast(pointer_)); + } + if (property == "adopted") { + return adopted_; + } + if (property == "takeRetainedValue" || property == "takeUnretainedValue") { + bool retained = property == "takeRetainedValue"; + std::weak_ptr weakSelf = shared_from_this(); + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property.c_str()), 0, + [weakSelf, retained](Runtime& runtime, const Value&, const Value*, + size_t) -> Value { + auto self = weakSelf.lock(); + if (!self || self->pointer_ == nullptr || self->consumed_) { + throw facebook::jsi::JSError(runtime, "Unmanaged value has already been consumed."); + } + id object = static_cast(self->pointer_); + self->consumed_ = true; + self->pointer_ = nullptr; + self->adopted_ = false; + return makeNativeObjectValue(runtime, self->bridge_, object, retained); + }); + } + if (property == "add" || property == "subtract") { + void* pointer = pointer_; + bool add = property == "add"; + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property.c_str()), 1, + [bridge, pointer, add](Runtime& runtime, const Value&, + const Value* args, size_t count) -> Value { + if (count < 1 || !args[0].isNumber()) { + throw facebook::jsi::JSError(runtime, "Pointer offset must be a number."); + } + intptr_t offset = static_cast(args[0].getNumber()); + intptr_t base = reinterpret_cast(pointer); + void* result = reinterpret_cast(add ? base + offset : base - offset); + return createPointer(runtime, bridge, result); + }); + } + if (property == "toNumber") { + void* pointer = pointer_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "toNumber"), 0, + [pointer](Runtime&, const Value&, const Value*, size_t) -> Value { + return static_cast(reinterpret_cast(pointer)); + }); + } + if (property == "toBigInt") { + void* pointer = pointer_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "toBigInt"), 0, + [pointer](Runtime& runtime, const Value&, const Value*, size_t) -> Value { + return BigInt::fromUint64( + runtime, + static_cast(reinterpret_cast(pointer))); + }); + } + if (property == "toHexString" || property == "toDecimalString") { + void* pointer = pointer_; + bool hex = property == "toHexString"; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property.c_str()), 0, + [pointer, hex](Runtime& runtime, const Value&, const Value*, size_t) -> Value { + if (hex) { + char text[2 + sizeof(uintptr_t) * 2 + 1] = {}; + snprintf(text, sizeof(text), "0x%llx", + static_cast( + reinterpret_cast(pointer))); + return makeString(runtime, text); + } else { + char text[32] = {}; + snprintf(text, sizeof(text), "%lld", + static_cast(reinterpret_cast(pointer))); + return makeString(runtime, text); + } + }); + } + if (property == "toString") { + void* pointer = pointer_; + std::string kind = kind_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "toString"), 0, + [pointer, kind](Runtime& runtime, const Value&, const Value*, + size_t) -> Value { + char address[32] = {}; + snprintf(address, sizeof(address), "%p", pointer); + if (kind == "pointer") { + return makeString(runtime, + ""); + } + return makeString(runtime, "[NativeApiJsi " + kind + " " + + std::string(address) + "]"); + }); + } + return Value::undefined(); + } + + std::vector getPropertyNames(Runtime& runtime) override { + std::vector names; + names.reserve(3); + addPropertyName(runtime, names, "kind"); + addPropertyName(runtime, names, "address"); + addPropertyName(runtime, names, "adopted"); + addPropertyName(runtime, names, "takeRetainedValue"); + addPropertyName(runtime, names, "takeUnretainedValue"); + addPropertyName(runtime, names, "add"); + addPropertyName(runtime, names, "subtract"); + addPropertyName(runtime, names, "toNumber"); + addPropertyName(runtime, names, "toBigInt"); + addPropertyName(runtime, names, "toHexString"); + addPropertyName(runtime, names, "toDecimalString"); + addPropertyName(runtime, names, "toString"); + return names; + } + + private: + std::shared_ptr bridge_; + void* pointer_ = nullptr; + std::string kind_; + bool adopted_ = false; + bool consumed_ = false; +}; + +class NativeApiReferenceHostObject final : public HostObject { + public: + NativeApiReferenceHostObject(std::shared_ptr bridge, + NativeApiJsiType type, void* data, bool ownsData, + size_t byteLength = 0, + std::shared_ptr pendingValue = nullptr, + std::shared_ptr backingValue = nullptr) + : bridge_(std::move(bridge)), + type_(std::move(type)), + data_(data), + ownsData_(ownsData), + byteLength_(byteLength), + pendingValue_(std::move(pendingValue)), + backingValue_(std::move(backingValue)) {} + + ~NativeApiReferenceHostObject() override { + if (ownsData_ && data_ != nullptr) { + free(data_); + data_ = nullptr; + } + } + + void* data() const { return data_; } + const NativeApiJsiType& type() const { return type_; } + void ensureStorage(Runtime& runtime, NativeApiJsiType type, + NativeApiJsiArgumentFrame& frame, size_t elements = 1); + + Value get(Runtime& runtime, const PropNameID& name) override; + void set(Runtime& runtime, const PropNameID& name, const Value& value) override; + std::vector getPropertyNames(Runtime& runtime) override { + std::vector names; + addPropertyName(runtime, names, "kind"); + addPropertyName(runtime, names, "value"); + addPropertyName(runtime, names, "address"); + addPropertyName(runtime, names, "toString"); + return names; + } + + private: + std::shared_ptr bridge_; + NativeApiJsiType type_; + void* data_ = nullptr; + bool ownsData_ = false; + size_t byteLength_ = 0; + std::shared_ptr pendingValue_; + std::shared_ptr backingValue_; +}; + +class NativeApiStructObjectHostObject final : public HostObject { + public: + NativeApiStructObjectHostObject( + std::shared_ptr bridge, + std::shared_ptr info, + const void* data = nullptr, bool ownsData = true, + std::shared_ptr> storageOwner = nullptr, + std::shared_ptr backingValue = nullptr) + : bridge_(std::move(bridge)), + info_(std::move(info)), + ownedData_(std::move(storageOwner)), + backingValue_(std::move(backingValue)), + ownsData_(ownsData) { + size_t size = info_ != nullptr ? info_->size : 0; + if (ownedData_ != nullptr) { + data_ = const_cast(data); + ownsData_ = false; + } else if (ownsData_) { + ownedData_ = std::make_shared>(size, 0); + if (data != nullptr && size > 0) { + std::memcpy(ownedData_->data(), data, size); + } + data_ = ownedData_->empty() ? nullptr : ownedData_->data(); + } else { + data_ = const_cast(data); + } + } + + void* data() const { return data_; } + std::shared_ptr info() const { return info_; } + std::shared_ptr> storageOwner() const { + return ownedData_; + } + std::shared_ptr backingValue() const { return backingValue_; } + + Value get(Runtime& runtime, const PropNameID& name) override; + void set(Runtime& runtime, const PropNameID& name, const Value& value) override; + std::vector getPropertyNames(Runtime& runtime) override; + + private: + std::shared_ptr bridge_; + std::shared_ptr info_; + std::shared_ptr> ownedData_; + std::shared_ptr backingValue_; + void* data_ = nullptr; + bool ownsData_ = true; +}; + +class NativeApiFastEnumerationIteratorHostObject final : public HostObject { + public: + NativeApiFastEnumerationIteratorHostObject( + std::shared_ptr bridge, id collection) + : bridge_(std::move(bridge)), collection_(collection) { + [collection_ retain]; + } + + ~NativeApiFastEnumerationIteratorHostObject() override { + [collection_ release]; + collection_ = nil; + } + + Value get(Runtime& runtime, const PropNameID& name) override { + std::string property = name.utf8(runtime); + if (property == "next") { + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "next"), 0, + [this](Runtime& runtime, const Value&, const Value*, size_t) -> Value { + return next(runtime); + }); + } + return Value::undefined(); + } + + std::vector getPropertyNames(Runtime& runtime) override { + std::vector names; + addPropertyName(runtime, names, "next"); + return names; + } + + private: + Value next(Runtime& runtime) { + Object result(runtime); + if (done_ || collection_ == nil) { + result.setProperty(runtime, "done", true); + return result; + } + + if (stackIndex_ >= stackLength_) { + stackLength_ = [collection_ countByEnumeratingWithState:&state_ + objects:stack_ + count:16]; + stackIndex_ = 0; + if (stackLength_ == 0) { + done_ = true; + result.setProperty(runtime, "done", true); + return result; + } + } + + id value = state_.itemsPtr[stackIndex_++]; + NativeApiJsiType valueType = nativeObjectReturnTypeForClass(object_getClass(value)); + result.setProperty(runtime, "value", + convertNativeReturnValue(runtime, bridge_, valueType, &value)); + result.setProperty(runtime, "done", false); + return result; + } + + std::shared_ptr bridge_; + id collection_ = nil; + NSFastEnumerationState state_ = {}; + id __unsafe_unretained stack_[16] = {}; + NSUInteger stackLength_ = 0; + NSUInteger stackIndex_ = 0; + bool done_ = false; +}; + +NativeApiSymbol nativeApiSymbolForRuntimeClass( + const std::shared_ptr& bridge, Class cls) { + const char* name = cls != Nil ? class_getName(cls) : ""; + if (bridge != nullptr) { + if (const NativeApiSymbol* symbol = bridge->findClassForRuntimePointer(cls)) { + return *symbol; + } + if (const NativeApiSymbol* symbol = bridge->findClassForRuntimeClass(cls)) { + return *symbol; + } + if (name != nullptr) { + if (const NativeApiSymbol* symbol = bridge->findClass(name)) { + return *symbol; + } + } + } + + return NativeApiSymbol{ + .kind = NativeApiSymbolKind::Class, + .offset = MD_SECTION_OFFSET_NULL, + .name = name != nullptr ? name : "", + .runtimeName = name != nullptr ? name : "", + }; +} + +class NativeApiSuperHostObject final : public HostObject { + public: + NativeApiSuperHostObject(std::shared_ptr bridge, + id receiver, Class dispatchClass) + : bridge_(std::move(bridge)), + receiver_(receiver), + dispatchClass_(dispatchClass) { + if (receiver_ != nil) { + [receiver_ retain]; + } + } + + ~NativeApiSuperHostObject() override { + if (receiver_ != nil) { + [receiver_ release]; + receiver_ = nil; + } + } + + Value get(Runtime& runtime, const PropNameID& name) override { + std::string property = name.utf8(runtime); + if (property == "kind") { + return makeString(runtime, "super"); + } + if (property == "toString") { + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "toString"), 0, + [](Runtime& runtime, const Value&, const Value*, size_t) -> Value { + return makeString(runtime, "[NativeApiJsiSuper]"); + }); + } + if (receiver_ == nil || dispatchClass_ == Nil) { + return Value::undefined(); + } + + if (const NativeApiSymbol* symbol = + bridge_->findClassForRuntimeClass(dispatchClass_)) { + const auto& members = bridge_->membersForClass(*symbol); + if (const NativeApiMember* propertyMember = + selectPropertyMember(members, property, false)) { + SEL selector = sel_getUid(propertyMember->selectorName.c_str()); + if (class_getInstanceMethod(dispatchClass_, selector) != nullptr) { + return callObjCSelector(runtime, bridge_, receiver_, false, + propertyMember->selectorName, propertyMember, + nullptr, 0, dispatchClass_); + } + } + + if (selectMethodMember(members, property, false, 0) != nullptr) { + auto bridge = bridge_; + id receiver = receiver_; + Class dispatchClass = dispatchClass_; + std::string memberName = property; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property.c_str()), 0, + [bridge, receiver, dispatchClass, memberName]( + Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + const NativeApiSymbol* symbol = + bridge->findClassForRuntimeClass(dispatchClass); + if (symbol == nullptr) { + throw facebook::jsi::JSError( + runtime, "Objective-C metadata is not available for super."); + } + const NativeApiMember* selected = selectMethodMember( + bridge->membersForClass(*symbol), memberName, false, count); + if (selected == nullptr) { + throw facebook::jsi::JSError( + runtime, "Objective-C super selector is not available: " + + memberName); + } + return callObjCSelector(runtime, bridge, receiver, false, + selected->selectorName, selected, args, + count, dispatchClass); + }); + } + } + + if (auto selectorName = + runtimeSelectorNameForProperty(dispatchClass_, false, property)) { + auto bridge = bridge_; + id receiver = receiver_; + Class dispatchClass = dispatchClass_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property.c_str()), 0, + [bridge, receiver, dispatchClass, selectorName = *selectorName]( + Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + return callObjCSelector(runtime, bridge, receiver, false, + selectorName, nullptr, args, count, + dispatchClass); + }); + } + + return Value::undefined(); + } + + void set(Runtime& runtime, const PropNameID& name, const Value& value) override { + std::string property = name.utf8(runtime); + if (receiver_ == nil || dispatchClass_ == Nil) { + throw facebook::jsi::JSError(runtime, "Cannot set property on nil super."); + } + + if (const NativeApiSymbol* symbol = + bridge_->findClassForRuntimeClass(dispatchClass_)) { + const auto& members = bridge_->membersForClass(*symbol); + if (const NativeApiMember* propertyMember = + selectWritablePropertyMember(members, property, false)) { + if (propertyMember->readonly || + propertyMember->setterSelectorName.empty()) { + throw facebook::jsi::JSError( + runtime, "Attempted to assign to readonly property."); + } + NativeApiMember setterMember = *propertyMember; + setterMember.selectorName = propertyMember->setterSelectorName; + setterMember.signatureOffset = propertyMember->setterSignatureOffset; + Value args[] = {Value(runtime, value)}; + callObjCSelector(runtime, bridge_, receiver_, false, + setterMember.selectorName, &setterMember, args, 1, + dispatchClass_); + return; + } + } + + std::string setterSelectorName = setterSelectorForProperty(property); + SEL selector = sel_getUid(setterSelectorName.c_str()); + if (class_getInstanceMethod(dispatchClass_, selector) != nullptr) { + Value args[] = {Value(runtime, value)}; + callObjCSelector(runtime, bridge_, receiver_, false, setterSelectorName, + nullptr, args, 1, dispatchClass_); + return; + } + + throw facebook::jsi::JSError(runtime, + "No writable native super property: " + + property); + } + + std::vector getPropertyNames(Runtime& runtime) override { + std::vector names; + addPropertyName(runtime, names, "kind"); + addPropertyName(runtime, names, "toString"); + return names; + } + + private: + std::shared_ptr bridge_; + id receiver_ = nil; + Class dispatchClass_ = Nil; +}; + +class NativeApiObjectHostObject final + : public HostObject, + public std::enable_shared_from_this { + public: + NativeApiObjectHostObject(std::shared_ptr bridge, + id object, bool ownsObject) + : bridge_(std::move(bridge)), object_(object), ownsObject_(ownsObject) { + if (object_ != nil && !ownsObject_) { + [object_ retain]; + ownsObject_ = true; + } + } + + ~NativeApiObjectHostObject() override { + if (ownsObject_ && object_ != nil) { + [object_ release]; + object_ = nil; + } + } + + id object() const { return object_; } + + void disownObject(id expected) { + if (object_ == expected) { + ownsObject_ = false; + object_ = nil; + } + } + + static bool isInitializerSelector(const std::string& selectorName) { + return selectorName.rfind("init", 0) == 0; + } + + static id nativeObjectFromValue(Runtime& runtime, const Value& value) { + if (!value.isObject()) { + return nil; + } + Object object = value.asObject(runtime); + if (!object.isHostObject(runtime)) { + return nil; + } + return object.getHostObject(runtime)->object(); + } + + Value callObjectSelector(Runtime& runtime, const std::string& selectorName, + const NativeApiMember* member, const Value* args, + size_t count, Class dispatchSuperClass = Nil) { + id receiver = object_; + if (receiver == nil) { + throw facebook::jsi::JSError(runtime, + "Cannot send Objective-C selector to nil."); + } + + const bool initializer = isInitializerSelector(selectorName); + std::optional classWrapper; + if (initializer) { + Value classWrapperValue = bridge_->findObjectExpando( + runtime, receiver, "__nativeApiClassWrapper"); + if (classWrapperValue.isObject()) { + classWrapper.emplace(classWrapperValue.asObject(runtime)); + } + bridge_->forgetRoundTripValue(receiver); + bridge_->forgetObjectExpandos(receiver); + } + + Value result = + callObjCSelector(runtime, bridge_, receiver, false, selectorName, member, + args, count, dispatchSuperClass); + if (initializer) { + if (nativeObjectFromValue(runtime, result) != receiver) { + disownObject(receiver); + } else if (classWrapper) { + bridge_->setObjectExpando(runtime, receiver, "__nativeApiClassWrapper", + Value(runtime, *classWrapper)); + } + } + return result; + } + + Value get(Runtime& runtime, const PropNameID& name) override { + std::string property = name.utf8(runtime); + if (property == "kind") { + return makeString(runtime, "object"); + } + if (property == "className") { + return makeString(runtime, object_ != nil ? object_getClassName(object_) : ""); + } + if (property == "nativeAddress") { + char address[32] = {}; + snprintf(address, sizeof(address), "%p", object_); + return makeString(runtime, address); + } + if (property == "class") { + auto bridge = bridge_; + id object = object_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "class"), 0, + [bridge, object](Runtime& runtime, const Value&, const Value*, + size_t) -> Value { + if (object == nil) { + return Value::undefined(); + } + Value classWrapper = bridge->findObjectExpando( + runtime, object, "__nativeApiClassWrapper"); + if (classWrapper.isObject()) { + return classWrapper; + } + NativeApiSymbol symbol = + nativeApiSymbolForRuntimeClass(bridge, object_getClass(object)); + return makeNativeClassValue(runtime, bridge, std::move(symbol)); + }); + } + if (property == "constructor") { + if (object_ == nil) { + return Value::undefined(); + } + Value classWrapper = bridge_->findObjectExpando( + runtime, object_, "__nativeApiClassWrapper"); + if (classWrapper.isObject()) { + return classWrapper; + } + NativeApiSymbol symbol = + nativeApiSymbolForRuntimeClass(bridge_, object_getClass(object_)); + return makeNativeClassValue(runtime, bridge_, std::move(symbol)); + } + if (property == "superclass") { + if (object_ == nil) { + return Value::undefined(); + } + Class superclass = class_getSuperclass(object_getClass(object_)); + if (superclass == Nil) { + return Value::null(); + } + NativeApiSymbol symbol = nativeApiSymbolForRuntimeClass(bridge_, superclass); + return makeNativeClassValue(runtime, bridge_, std::move(symbol)); + } + if (property == "super") { + Class dispatchClass = + object_ != nil ? class_getSuperclass(object_getClass(object_)) : Nil; + return Object::createFromHostObject( + runtime, + std::make_shared(bridge_, object_, + dispatchClass)); + } + if (property == "invoke" || property == "send") { + auto bridge = bridge_; + id object = object_; + std::weak_ptr weakSelf = shared_from_this(); + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property.c_str()), 1, + [bridge, object, weakSelf](Runtime& runtime, const Value&, + const Value* args, + size_t count) -> Value { + std::string selectorName = + readStringArg(runtime, args, count, 0, "selector"); + if (auto self = weakSelf.lock()) { + return self->callObjectSelector(runtime, selectorName, nullptr, + args + 1, count - 1); + } + return callObjCSelector(runtime, bridge, object, false, selectorName, + nullptr, args + 1, count - 1); + }); + } + if (property == "takeRetainedValue" || property == "takeUnretainedValue") { + bool retained = property == "takeRetainedValue"; + std::weak_ptr weakSelf = shared_from_this(); + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property.c_str()), 0, + [weakSelf, retained](Runtime& runtime, const Value&, const Value*, + size_t) -> Value { + auto self = weakSelf.lock(); + if (!self || self->object_ == nil || self->consumed_) { + throw facebook::jsi::JSError(runtime, "Unmanaged value has already been consumed."); + } + + id object = self->object_; + if (self->bridge_ != nullptr) { + self->bridge_->forgetRoundTripValue(object); + } + if (self->ownsObject_) { + [object release]; + } + self->object_ = nil; + self->ownsObject_ = false; + self->consumed_ = true; + return makeNativeObjectValue(runtime, self->bridge_, object, retained); + }); + } + if (property == "toString") { + id object = object_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "toString"), 0, + [object](Runtime& runtime, const Value&, const Value*, size_t) -> Value { + NSString* description = + object != nil ? [object description] : @""; + return makeString(runtime, description.UTF8String ?: ""); + }); + } + if (property == "URL" && object_ != nil && + [object_ respondsToSelector:@selector(URL)]) { + return callObjectSelector(runtime, "URL", nullptr, nullptr, 0); + } + if (property == "Symbol.iterator" || + property == "Symbol(Symbol.iterator)" || + property == "@@iterator") { + auto bridge = bridge_; + id object = object_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "Symbol.iterator"), 0, + [bridge, object](Runtime& runtime, const Value&, const Value*, + size_t) -> Value { + if (object == nil || + ![object conformsToProtocol:@protocol(NSFastEnumeration)]) { + throw facebook::jsi::JSError( + runtime, "Object does not conform to NSFastEnumeration."); + } + return Object::createFromHostObject( + runtime, + std::make_shared( + bridge, static_cast>(object))); + }); + } + +#if TARGET_OS_OSX + if (property == "initWithRedGreenBlueAlpha") { + Class nsColorClass = NSClassFromString(@"NSColor"); + if (object_ != nil && nsColorClass != Nil && + [object_ isKindOfClass:nsColorClass]) { + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property.c_str()), 4, + [bridge, nsColorClass](Runtime& runtime, const Value&, + const Value* args, size_t count) -> Value { + const char* selectors[] = { + "colorWithSRGBRed:green:blue:alpha:", + "colorWithCalibratedRed:green:blue:alpha:", + "colorWithDeviceRed:green:blue:alpha:", + }; + for (const char* selectorName : selectors) { + if (class_getClassMethod(nsColorClass, + sel_getUid(selectorName)) != nullptr) { + return callObjCSelector(runtime, bridge, + static_cast(nsColorClass), true, + selectorName, nullptr, args, count); + } + } + throw facebook::jsi::JSError( + runtime, "NSColor RGB initializer is not available."); + }); + } + } +#endif + + if (property == "initWithFireDateIntervalTargetSelectorUserInfoRepeats") { + Class timerClass = NSClassFromString(@"NSTimer"); + if (object_ != nil && timerClass != Nil && + [object_ isKindOfClass:timerClass]) { + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property.c_str()), 6, + [bridge, timerClass](Runtime& runtime, const Value&, + const Value* args, size_t count) -> Value { + if (count < 6) { + throw facebook::jsi::JSError( + runtime, "NSTimer initializer expects six arguments."); + } + return callObjCSelector( + runtime, bridge, static_cast(timerClass), true, + "timerWithTimeInterval:target:selector:userInfo:repeats:", + nullptr, args + 1, count - 1); + }); + } + } + + Value expando = bridge_->findObjectExpando(runtime, object_, property); + if (!expando.isUndefined()) { + return expando; + } + + if (object_ != nil) { + try { + Value receiver = bridge_->findRoundTripValue(runtime, object_); + Value resolverValue = runtime.global().getProperty( + runtime, "__nativeScriptGetNativeApiPrototypeProperty"); + if (receiver.isObject() && resolverValue.isObject() && + resolverValue.asObject(runtime).isFunction(runtime)) { + Value prototype = + bridge_->findClassPrototype(runtime, object_getClass(object_)); + Value prototypeOrName = prototype.isObject() + ? Value(runtime, prototype) + : Value::undefined(); + if (prototypeOrName.isUndefined()) { + Value classWrapper = bridge_->findObjectExpando( + runtime, object_, "__nativeApiClassWrapper"); + if (classWrapper.isObject()) { + Object wrapperObject = classWrapper.asObject(runtime); + Value wrapperPrototype = + wrapperObject.getProperty(runtime, "prototype"); + if (wrapperPrototype.isObject()) { + prototypeOrName = std::move(wrapperPrototype); + } + } + } + if (prototypeOrName.isUndefined()) { + const char* className = object_getClassName(object_); + prototypeOrName = makeString(runtime, + className != nullptr ? className : ""); + } + Value resolved = resolverValue.asObject(runtime) + .asFunction(runtime) + .call(runtime, std::move(prototypeOrName), + Value(runtime, receiver), + makeString(runtime, property)); + if (resolved.isObject()) { + Object result = resolved.asObject(runtime); + Value found = result.getProperty(runtime, "found"); + if (found.isBool() && found.getBool()) { + return result.getProperty(runtime, "value"); + } + } + } + } catch (const std::exception&) { + } + } + + if (object_ != nil && [object_ isKindOfClass:[NSArray class]]) { + NSArray* array = static_cast(object_); + if (property == "length") { + return static_cast(array.count); + } + if (auto index = parseArrayIndexProperty(property)) { + if (*index >= array.count) { + return Value::undefined(); + } + id element = [array objectAtIndex:*index]; + NativeApiJsiType elementType = nativeObjectReturnType(); + return convertNativeReturnValue(runtime, bridge_, elementType, &element); + } + } + + if (object_ != nil) { + if (const NativeApiSymbol* symbol = + bridge_->findClassForRuntimeClass(object_getClass(object_))) { + const auto& members = bridge_->membersForClass(*symbol); + if (const NativeApiMember* propertyMember = + selectPropertyMember(members, property, false)) { + SEL selector = sel_getUid(propertyMember->selectorName.c_str()); + if ([object_ respondsToSelector:selector]) { + return callObjectSelector(runtime, propertyMember->selectorName, + propertyMember, nullptr, 0); + } + std::string booleanSelectorName = + booleanGetterSelectorForProperty(property); + if (booleanSelectorName != propertyMember->selectorName) { + SEL booleanSelector = sel_getUid(booleanSelectorName.c_str()); + if ([object_ respondsToSelector:booleanSelector]) { + NativeApiMember getterMember = *propertyMember; + getterMember.selectorName = booleanSelectorName; + return callObjectSelector(runtime, getterMember.selectorName, + &getterMember, nullptr, 0); + } + } + } + + if (selectMethodMember(members, property, false, 0) != nullptr) { + auto bridge = bridge_; + id object = object_; + std::weak_ptr weakSelf = + shared_from_this(); + std::string memberName = property; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property.c_str()), + 0, + [bridge, object, weakSelf, memberName](Runtime& runtime, + const Value&, + const Value* args, + size_t count) -> Value { + const NativeApiSymbol* symbol = + bridge->findClassForRuntimeClass(object_getClass(object)); + if (symbol == nullptr) { + throw facebook::jsi::JSError( + runtime, "Objective-C metadata is not available for object."); + } + const NativeApiMember* selected = selectMethodMember( + bridge->membersForClass(*symbol), memberName, false, count); + if (selected == nullptr) { + throw facebook::jsi::JSError( + runtime, "Objective-C selector is not available: " + + memberName); + } + if (auto self = weakSelf.lock()) { + return self->callObjectSelector( + runtime, selected->selectorName, selected, args, count); + } + return callObjCSelector(runtime, bridge, object, false, + selected->selectorName, selected, args, + count); + }); + } + } + + if (auto selectorName = + runtimeSelectorNameForProperty(object_getClass(object_), false, + property)) { + if (selectorArgumentCount(*selectorName) == 0 && + hasRuntimeSetterForProperty(object_getClass(object_), false, + property)) { + return callObjectSelector(runtime, *selectorName, nullptr, nullptr, 0); + } + + auto bridge = bridge_; + id object = object_; + std::weak_ptr weakSelf = shared_from_this(); + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property.c_str()), 0, + [bridge, object, weakSelf, selectorName = *selectorName]( + Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + if (auto self = weakSelf.lock()) { + return self->callObjectSelector(runtime, selectorName, nullptr, + args, count); + } + return callObjCSelector(runtime, bridge, object, false, + selectorName, nullptr, args, count); + }); + } + + if ([object_ isKindOfClass:[NSDictionary class]]) { + NSString* key = [NSString stringWithUTF8String:property.c_str()]; + if (key != nil) { + id value = [static_cast(object_) objectForKey:key]; + if (value != nil) { + NativeApiJsiType valueType = nativeObjectReturnType(); + return convertNativeReturnValue(runtime, bridge_, valueType, &value); + } + } + } + } + + return Value::undefined(); + } + + void set(Runtime& runtime, const PropNameID& name, const Value& value) override { + std::string property = name.utf8(runtime); + if (object_ == nil) { + throw facebook::jsi::JSError(runtime, "Cannot set property on nil object."); + } + + if (const NativeApiSymbol* symbol = + bridge_->findClassForRuntimeClass(object_getClass(object_))) { + const auto& members = bridge_->membersForClass(*symbol); + if (const NativeApiMember* propertyMember = + selectWritablePropertyMember(members, property, false)) { + if (propertyMember->readonly) { + throw facebook::jsi::JSError( + runtime, "Attempted to assign to readonly property."); + } + NativeApiMember setterMember = *propertyMember; + setterMember.selectorName = propertyMember->setterSelectorName; + setterMember.signatureOffset = propertyMember->setterSignatureOffset; + Value args[] = {Value(runtime, value)}; + callObjCSelector(runtime, bridge_, object_, false, + setterMember.selectorName, &setterMember, args, 1); + return; + } + } + + std::string setterSelectorName = setterSelectorForProperty(property); + SEL selector = sel_getUid(setterSelectorName.c_str()); + if ([object_ respondsToSelector:selector]) { + Value args[] = {Value(runtime, value)}; + callObjCSelector(runtime, bridge_, object_, false, setterSelectorName, + nullptr, args, 1); + return; + } + + bridge_->setObjectExpando(runtime, object_, property, value); + } + + std::vector getPropertyNames(Runtime& runtime) override { + std::vector names; + names.reserve(6); + addPropertyName(runtime, names, "kind"); + addPropertyName(runtime, names, "className"); + addPropertyName(runtime, names, "nativeAddress"); + addPropertyName(runtime, names, "constructor"); + addPropertyName(runtime, names, "superclass"); + addPropertyName(runtime, names, "super"); + addPropertyName(runtime, names, "invoke"); + addPropertyName(runtime, names, "send"); + addPropertyName(runtime, names, "takeRetainedValue"); + addPropertyName(runtime, names, "takeUnretainedValue"); + addPropertyName(runtime, names, "toString"); + return names; + } + + private: + std::shared_ptr bridge_; + id object_ = nil; + bool ownsObject_ = false; + bool consumed_ = false; +}; + +class NativeApiClassHostObject final : public HostObject { + public: + NativeApiClassHostObject(std::shared_ptr bridge, + NativeApiSymbol symbol) + : bridge_(std::move(bridge)), symbol_(std::move(symbol)) {} + + Class nativeClass() const { + return objc_lookUpClass(symbol_.runtimeName.c_str()); + } + + Value get(Runtime& runtime, const PropNameID& name) override { + std::string property = name.utf8(runtime); + if (property == "kind") { + return makeString(runtime, "class"); + } + if (property == "name") { + return makeString(runtime, symbol_.name); + } + if (property == "runtimeName") { + return makeString(runtime, symbol_.runtimeName); + } + if (property == "available") { + return objc_lookUpClass(symbol_.runtimeName.c_str()) != nil; + } + if (property == "metadataOffset") { + return static_cast(symbol_.offset); + } + if (property == "__superclass") { + if (symbol_.superclassOffset == MD_SECTION_OFFSET_NULL) { + return Value::undefined(); + } + const NativeApiSymbol* superclass = + bridge_->findClassByOffset(symbol_.superclassOffset); + if (superclass == nullptr) { + return Value::undefined(); + } + return makeNativeClassValue(runtime, bridge_, *superclass); + } + if (property == "__staticMembers" || property == "__instanceMembers") { + bool staticMembers = property == "__staticMembers"; + const auto& members = bridge_->surfaceMembersForClass(symbol_); + Array result(runtime, members.size()); + size_t index = 0; + for (const auto& member : members) { + bool memberIsStatic = + (member.flags & metagen::mdMemberStatic) != 0; + if (memberIsStatic != staticMembers) { + continue; + } + Object descriptor(runtime); + descriptor.setProperty(runtime, "name", makeString(runtime, member.name)); + descriptor.setProperty(runtime, "selectorName", + makeString(runtime, member.selectorName)); + descriptor.setProperty( + runtime, "argumentCount", + static_cast(selectorArgumentCount(member.selectorName))); + descriptor.setProperty(runtime, "property", member.property); + descriptor.setProperty(runtime, "readonly", member.readonly); + descriptor.setProperty(runtime, "setterSelectorName", + makeString(runtime, member.setterSelectorName)); + result.setValueAtIndex(runtime, index++, descriptor); + } + Array compact(runtime, index); + for (size_t i = 0; i < index; i++) { + compact.setValueAtIndex(runtime, i, result.getValueAtIndex(runtime, i)); + } + return compact; + } + if (property == "toString") { + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "toString"), 0, + [symbol = symbol_](Runtime& runtime, const Value&, + const Value*, size_t) -> Value { + return makeString(runtime, + "[NativeApiJsiClass " + symbol.name + "]"); + }); + } + if (property == "construct" || property == "alloc" || property == "new") { + auto bridge = bridge_; + auto symbol = symbol_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property), 0, + [bridge, symbol, property](Runtime& runtime, const Value&, + const Value* args, size_t count) -> Value { + Class cls = objc_lookUpClass(symbol.runtimeName.c_str()); + if (cls == nil) { + throw facebook::jsi::JSError( + runtime, "Objective-C class is not available: " + symbol.name); + } + + id result = nil; + if (property == "construct" && count == 1) { + void* pointer = nullptr; + if (args[0].isNumber()) { + pointer = reinterpret_cast( + static_cast(args[0].getNumber())); + } else if (args[0].isObject()) { + Object object = args[0].asObject(runtime); + if (object.isHostObject(runtime)) { + pointer = object + .getHostObject( + runtime) + ->pointer(); + } else if (object.isHostObject( + runtime)) { + pointer = object + .getHostObject( + runtime) + ->data(); + } else if (object.isHostObject( + runtime)) { + pointer = object + .getHostObject( + runtime) + ->object(); + } + } + return makeNativeObjectValue(runtime, bridge, + static_cast(pointer), false); + } + + if (property == "new") { + if (count != 0) { + throw facebook::jsi::JSError( + runtime, "new does not take arguments; use invoke for an " + "explicit Objective-C selector."); + } + result = [cls new]; + } else { + if (count != 0) { + throw facebook::jsi::JSError( + runtime, "alloc does not take arguments; call invoke on the " + "allocated object for an explicit init selector."); + } + result = [cls alloc]; + } + + return makeNativeObjectValue(runtime, bridge, result, true); + }); + } + if (property == "invoke" || property == "send") { + auto bridge = bridge_; + auto symbol = symbol_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property.c_str()), 1, + [bridge, symbol](Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + std::string selectorName = + readStringArg(runtime, args, count, 0, "selector"); + Class cls = objc_lookUpClass(symbol.runtimeName.c_str()); + if (cls == nil) { + throw facebook::jsi::JSError( + runtime, "Objective-C class is not available: " + symbol.name); + } + return callObjCSelector(runtime, bridge, static_cast(cls), true, + selectorName, nullptr, args + 1, + count - 1); + }); + } + + const auto& members = bridge_->membersForClass(symbol_); + if (const NativeApiMember* propertyMember = + selectWritablePropertyMember(members, property, true)) { + auto bridge = bridge_; + auto symbol = symbol_; + Class cls = objc_lookUpClass(symbol.runtimeName.c_str()); + if (cls == nil) { + throw facebook::jsi::JSError( + runtime, "Objective-C class is not available: " + symbol.name); + } + SEL selector = sel_getUid(propertyMember->selectorName.c_str()); + if (class_getClassMethod(cls, selector) != nullptr) { + return callObjCSelector(runtime, bridge, static_cast(cls), true, + propertyMember->selectorName, propertyMember, + nullptr, 0); + } + } + + if (selectMethodMember(members, property, true, 0) != nullptr) { + auto bridge = bridge_; + auto symbol = symbol_; + std::string memberName = property; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property.c_str()), 0, + [bridge, symbol, memberName](Runtime& runtime, const Value&, + const Value* args, + size_t count) -> Value { + Class cls = objc_lookUpClass(symbol.runtimeName.c_str()); + if (cls == nil) { + throw facebook::jsi::JSError( + runtime, "Objective-C class is not available: " + symbol.name); + } + const NativeApiMember* selected = selectMethodMember( + bridge->membersForClass(symbol), memberName, true, count); + if (selected == nullptr) { + throw facebook::jsi::JSError( + runtime, "Objective-C selector is not available: " + + memberName); + } + return callObjCSelector(runtime, bridge, static_cast(cls), true, + selected->selectorName, selected, args, + count); + }); + } + + Class cls = objc_lookUpClass(symbol_.runtimeName.c_str()); + if (cls != nil) { + if (auto selectorName = + runtimeSelectorNameForProperty(cls, true, property)) { + if (selectorArgumentCount(*selectorName) == 0 && + hasRuntimeSetterForProperty(cls, true, property)) { + return callObjCSelector(runtime, bridge_, static_cast(cls), true, + *selectorName, nullptr, nullptr, 0); + } + + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, property.c_str()), 0, + [bridge, cls, selectorName = *selectorName]( + Runtime& runtime, const Value&, const Value* args, + size_t count) -> Value { + return callObjCSelector(runtime, bridge, static_cast(cls), + true, selectorName, nullptr, args, + count); + }); + } + } + + return Value::undefined(); + } + + void set(Runtime& runtime, const PropNameID& name, const Value& value) override { + std::string property = name.utf8(runtime); + Class cls = objc_lookUpClass(symbol_.runtimeName.c_str()); + if (cls == nil) { + throw facebook::jsi::JSError( + runtime, "Objective-C class is not available: " + symbol_.name); + } + + const auto& members = bridge_->membersForClass(symbol_); + if (const NativeApiMember* propertyMember = + selectPropertyMember(members, property, true)) { + if (propertyMember->readonly) { + throw facebook::jsi::JSError( + runtime, "Attempted to assign to readonly property."); + } + NativeApiMember setterMember = *propertyMember; + setterMember.selectorName = propertyMember->setterSelectorName; + setterMember.signatureOffset = propertyMember->setterSignatureOffset; + Value args[] = {Value(runtime, value)}; + callObjCSelector(runtime, bridge_, static_cast(cls), true, + setterMember.selectorName, &setterMember, args, 1); + return; + } + + std::string setterSelectorName = setterSelectorForProperty(property); + SEL selector = sel_getUid(setterSelectorName.c_str()); + if (class_getClassMethod(cls, selector) != nullptr) { + Value args[] = {Value(runtime, value)}; + callObjCSelector(runtime, bridge_, static_cast(cls), true, + setterSelectorName, nullptr, args, 1); + return; + } + + throw facebook::jsi::JSError(runtime, + "No writable native property: " + property); + } + + std::vector getPropertyNames(Runtime& runtime) override { + std::vector names; + names.reserve(8); + addPropertyName(runtime, names, "kind"); + addPropertyName(runtime, names, "name"); + addPropertyName(runtime, names, "runtimeName"); + addPropertyName(runtime, names, "available"); + addPropertyName(runtime, names, "metadataOffset"); + addPropertyName(runtime, names, "toString"); + addPropertyName(runtime, names, "construct"); + addPropertyName(runtime, names, "alloc"); + addPropertyName(runtime, names, "new"); + addPropertyName(runtime, names, "invoke"); + addPropertyName(runtime, names, "send"); + return names; + } + + private: + std::shared_ptr bridge_; + NativeApiSymbol symbol_; +}; + +Value makeNativeObjectValue(Runtime& runtime, + const std::shared_ptr& bridge, + id object, bool ownsObject) { + if (object == nil) { + return Value::null(); + } + + Value cached = bridge->findRoundTripValue(runtime, object); + if (!cached.isUndefined()) { + if (ownsObject) { + [object release]; + } + return cached; + } + + Object result = Object::createFromHostObject( + runtime, + std::make_shared(bridge, object, ownsObject)); + bridge->rememberRoundTripValue(runtime, object, Value(runtime, result)); + return result; +} + +Value globalNativeSymbolValue(Runtime& runtime, const NativeApiSymbol& symbol, + const char* expectedKind) { + Object global = runtime.global(); + Value cacheValue = global.getProperty( + runtime, "__nativeScriptNativeApiGlobalCache"); + if (!cacheValue.isObject()) { + return Value::undefined(); + } + + Object cache = cacheValue.asObject(runtime); + auto readCache = [&](const std::string& name) -> Value { + if (name.empty()) { + return Value::undefined(); + } + + Value value = cache.getProperty(runtime, name.c_str()); + if (!value.isObject()) { + return Value::undefined(); + } + + try { + Object object = value.asObject(runtime); + Value kindValue = object.getProperty(runtime, "kind"); + if (kindValue.isString() && + kindValue.asString(runtime).utf8(runtime) == expectedKind) { + return value; + } + } catch (const std::exception&) { + } + + return Value::undefined(); + }; + + Value value = readCache(symbol.name); + if (!value.isUndefined()) { + return value; + } + if (symbol.runtimeName != symbol.name) { + value = readCache(symbol.runtimeName); + if (!value.isUndefined()) { + return value; + } + } + + try { + if (std::strcmp(expectedKind, "class") == 0) { + Value classResolverValue = global.getProperty( + runtime, "__nativeScriptResolveNativeApiClassWrapper"); + if (classResolverValue.isObject() && + classResolverValue.asObject(runtime).isFunction(runtime)) { + Function classResolver = + classResolverValue.asObject(runtime).asFunction(runtime); + auto resolveClassWrapper = [&](const std::string& name) -> Value { + if (name.empty()) { + return Value::undefined(); + } + Value resolved = classResolver.call(runtime, makeString(runtime, name)); + return resolved.isObject() ? std::move(resolved) : Value::undefined(); + }; + + value = resolveClassWrapper(symbol.name); + if (!value.isUndefined()) { + return value; + } + if (symbol.runtimeName != symbol.name) { + value = resolveClassWrapper(symbol.runtimeName); + if (!value.isUndefined()) { + return value; + } + } + } + } + + Value resolverValue = + global.getProperty(runtime, "__nativeScriptResolveNativeApiGlobal"); + if (resolverValue.isObject() && + resolverValue.asObject(runtime).isFunction(runtime)) { + Function resolver = resolverValue.asObject(runtime).asFunction(runtime); + auto resolveGlobal = [&](const std::string& name) -> Value { + if (name.empty()) { + return Value::undefined(); + } + Value resolved = resolver.call(runtime, makeString(runtime, name), + makeString(runtime, expectedKind)); + if (resolved.isObject()) { + return resolved; + } + return Value::undefined(); + }; + + value = resolveGlobal(symbol.name); + if (!value.isUndefined()) { + return value; + } + if (symbol.runtimeName != symbol.name) { + value = resolveGlobal(symbol.runtimeName); + if (!value.isUndefined()) { + return value; + } + } + } + } catch (const std::exception&) { + } + + return Value::undefined(); +} + +Value makeNativeClassValue(Runtime& runtime, + const std::shared_ptr& bridge, + NativeApiSymbol symbol) { + Class cls = objc_lookUpClass(symbol.runtimeName.c_str()); + Value cachedClass = bridge->findClassValue(runtime, cls); + if (!cachedClass.isUndefined()) { + return cachedClass; + } + Value globalValue = globalNativeSymbolValue(runtime, symbol, "class"); + if (!globalValue.isUndefined()) { + return globalValue; + } + return Object::createFromHostObject( + runtime, + std::make_shared(bridge, std::move(symbol))); +} + +Protocol* lookupProtocolByNativeName(const std::string& name) { + Protocol* protocol = objc_getProtocol(name.c_str()); + if (protocol != nullptr) { + return protocol; + } + constexpr const char* suffix = "Protocol"; + size_t suffixLength = std::strlen(suffix); + if (name.size() > suffixLength && + name.compare(name.size() - suffixLength, suffixLength, suffix) == 0) { + protocol = objc_getProtocol( + name.substr(0, name.size() - suffixLength).c_str()); + } + return protocol; +} + +class NativeApiProtocolHostObject final : public HostObject { + public: + NativeApiProtocolHostObject(std::shared_ptr bridge, + NativeApiSymbol symbol) + : bridge_(std::move(bridge)), symbol_(std::move(symbol)) {} + + Protocol* nativeProtocol() const { + Protocol* protocol = lookupProtocolByNativeName(symbol_.runtimeName); + if (protocol == nullptr && symbol_.runtimeName != symbol_.name) { + protocol = lookupProtocolByNativeName(symbol_.name); + } + return protocol; + } + + const NativeApiSymbol& symbol() const { return symbol_; } + + Value get(Runtime& runtime, const PropNameID& name) override { + std::string property = name.utf8(runtime); + if (property == "kind") { + return makeString(runtime, "protocol"); + } + if (property == "name") { + return makeString(runtime, symbol_.name); + } + if (property == "runtimeName") { + return makeString(runtime, symbol_.runtimeName); + } + if (property == "available") { + return nativeProtocol() != nullptr; + } + if (property == "metadataOffset") { + return static_cast(symbol_.offset); + } + if (property == "nativeAddress") { + return static_cast( + reinterpret_cast(nativeProtocol())); + } + if (property == "prototype") { + Object prototype(runtime); + for (const auto& member : bridge_->membersForProtocol(symbol_)) { + if (prototype.hasProperty(runtime, member.name.c_str())) { + continue; + } + if (member.property) { + defineProtocolProperty(runtime, prototype, member, false); + } else { + prototype.setProperty(runtime, member.name.c_str(), + makeProtocolMemberFunction(runtime, member, + false)); + } + } + return prototype; + } + if (property == "toString") { + auto symbol = symbol_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "toString"), 0, + [symbol](Runtime& runtime, const Value&, const Value*, size_t) -> Value { + return makeString(runtime, + "[NativeApiJsiProtocol " + symbol.name + "]"); + }); + } + const auto& members = bridge_->membersForProtocol(symbol_); + if (const NativeApiMember* propertyMember = + selectPropertyMember(members, property, true)) { + return makeProtocolPropertyGetter(runtime, *propertyMember, true); + } + if (const NativeApiMember* propertyMember = + selectPropertyMember(members, property, false)) { + return makeProtocolPropertyGetter(runtime, *propertyMember, true); + } + if (const NativeApiMember* methodMember = + selectMethodMember(members, property, true, 0)) { + return makeProtocolMemberFunction(runtime, *methodMember, true); + } + if (const NativeApiMember* methodMember = + selectMethodMember(members, property, false, 0)) { + return makeProtocolMemberFunction(runtime, *methodMember, true); + } + return Value::undefined(); + } + + std::vector getPropertyNames(Runtime& runtime) override { + std::vector names; + addPropertyName(runtime, names, "kind"); + addPropertyName(runtime, names, "name"); + addPropertyName(runtime, names, "runtimeName"); + addPropertyName(runtime, names, "available"); + addPropertyName(runtime, names, "metadataOffset"); + addPropertyName(runtime, names, "nativeAddress"); + addPropertyName(runtime, names, "prototype"); + addPropertyName(runtime, names, "toString"); + for (const auto& member : bridge_->membersForProtocol(symbol_)) { + addPropertyName(runtime, names, member.name.c_str()); + } + return names; + } + + private: + static Class classReceiverFromThis(Runtime& runtime, const Value& thisValue) { + if (!thisValue.isObject()) { + return Nil; + } + + Object object = thisValue.asObject(runtime); + if (object.isHostObject(runtime)) { + return object.getHostObject(runtime)->nativeClass(); + } + + Value wrappedClass = object.getProperty(runtime, "__nativeApiClass"); + if (wrappedClass.isObject()) { + Object wrappedObject = wrappedClass.asObject(runtime); + if (wrappedObject.isHostObject(runtime)) { + return wrappedObject.getHostObject(runtime) + ->nativeClass(); + } + } + + Value kindValue = object.getProperty(runtime, "kind"); + if (kindValue.isString() && + kindValue.asString(runtime).utf8(runtime) == "class") { + Value runtimeNameValue = object.getProperty(runtime, "runtimeName"); + if (!runtimeNameValue.isString()) { + runtimeNameValue = object.getProperty(runtime, "name"); + } + if (runtimeNameValue.isString()) { + std::string runtimeName = + runtimeNameValue.asString(runtime).utf8(runtime); + return objc_lookUpClass(runtimeName.c_str()); + } + } + + return Nil; + } + + id objectReceiverFromThis(Runtime& runtime, const Value& thisValue) const { + if (!thisValue.isObject()) { + return nil; + } + + Object object = thisValue.asObject(runtime); + if (object.isHostObject(runtime)) { + return object.getHostObject(runtime)->object(); + } + + return nil; + } + + Value makeProtocolMemberFunction(Runtime& runtime, NativeApiMember member, + bool receiverIsClass) const { + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, member.name.c_str()), 0, + [bridge, member, receiverIsClass](Runtime& runtime, + const Value& thisValue, + const Value* args, + size_t count) -> Value { + id receiver = nil; + if (receiverIsClass) { + receiver = static_cast( + classReceiverFromThis(runtime, thisValue)); + } else if (thisValue.isObject()) { + Object object = thisValue.asObject(runtime); + if (object.isHostObject(runtime)) { + receiver = object.getHostObject(runtime) + ->object(); + } + } + + if (receiver == nil) { + throw facebook::jsi::JSError( + runtime, "Protocol member requires a native receiver."); + } + return callObjCSelector(runtime, bridge, receiver, receiverIsClass, + member.selectorName, &member, args, count); + }); + } + + Value makeProtocolPropertyGetter(Runtime& runtime, NativeApiMember member, + bool receiverIsClass) const { + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, member.name.c_str()), 0, + [bridge, member, receiverIsClass](Runtime& runtime, + const Value& thisValue, + const Value*, size_t) -> Value { + id receiver = nil; + if (receiverIsClass) { + receiver = static_cast( + classReceiverFromThis(runtime, thisValue)); + } else if (thisValue.isObject()) { + Object object = thisValue.asObject(runtime); + if (object.isHostObject(runtime)) { + receiver = object.getHostObject(runtime) + ->object(); + } + } + + if (receiver == nil) { + throw facebook::jsi::JSError( + runtime, "Protocol property requires a native receiver."); + } + return callObjCSelector(runtime, bridge, receiver, receiverIsClass, + member.selectorName, &member, nullptr, 0); + }); + } + + Value makeProtocolPropertySetter(Runtime& runtime, NativeApiMember member, + bool receiverIsClass) const { + auto bridge = bridge_; + return Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, member.setterSelectorName.c_str()), + 1, + [bridge, member, receiverIsClass](Runtime& runtime, + const Value& thisValue, + const Value* args, + size_t count) -> Value { + id receiver = nil; + if (receiverIsClass) { + receiver = static_cast( + classReceiverFromThis(runtime, thisValue)); + } else if (thisValue.isObject()) { + Object object = thisValue.asObject(runtime); + if (object.isHostObject(runtime)) { + receiver = object.getHostObject(runtime) + ->object(); + } + } + + if (receiver == nil) { + throw facebook::jsi::JSError( + runtime, "Protocol property requires a native receiver."); + } + if (count < 1) { + throw facebook::jsi::JSError( + runtime, "Protocol property setter expects a value."); + } + + NativeApiMember setterMember = member; + setterMember.selectorName = member.setterSelectorName; + setterMember.signatureOffset = member.setterSignatureOffset; + return callObjCSelector(runtime, bridge, receiver, receiverIsClass, + setterMember.selectorName, &setterMember, + args, 1); + }); + } + + void defineProtocolProperty(Runtime& runtime, Object& target, + const NativeApiMember& member, + bool receiverIsClass) const { + try { + Object objectCtor = runtime.global().getPropertyAsObject(runtime, "Object"); + Function defineProperty = + objectCtor.getPropertyAsFunction(runtime, "defineProperty"); + Object descriptor(runtime); + descriptor.setProperty(runtime, "configurable", true); + descriptor.setProperty(runtime, "enumerable", true); + descriptor.setProperty(runtime, "get", + makeProtocolPropertyGetter(runtime, member, + receiverIsClass)); + if (!member.readonly && !member.setterSelectorName.empty()) { + descriptor.setProperty(runtime, "set", + makeProtocolPropertySetter(runtime, member, + receiverIsClass)); + } + defineProperty.call(runtime, target, makeString(runtime, member.name), + descriptor); + } catch (const std::exception&) { + } + } + + std::shared_ptr bridge_; + NativeApiSymbol symbol_; +}; + +Value makeNativeProtocolValue(Runtime& runtime, + const std::shared_ptr& bridge, + NativeApiSymbol symbol) { + Value globalValue = globalNativeSymbolValue(runtime, symbol, "protocol"); + if (!globalValue.isUndefined()) { + return globalValue; + } + return Object::createFromHostObject( + runtime, + std::make_shared(bridge, std::move(symbol))); +} + +Class nativeClassFromJsiObject(Runtime& runtime, const Object& object) { + if (object.isHostObject(runtime)) { + return object.getHostObject(runtime)->nativeClass(); + } + + Value wrappedClass = object.getProperty(runtime, "__nativeApiClass"); + if (wrappedClass.isObject()) { + Object wrappedObject = wrappedClass.asObject(runtime); + if (wrappedObject.isHostObject(runtime)) { + return wrappedObject.getHostObject(runtime) + ->nativeClass(); + } + } + return Nil; +} diff --git a/NativeScript/ffi/shared/jsi/NativeApiJsiInstall.h b/NativeScript/ffi/shared/jsi/NativeApiJsiInstall.h new file mode 100644 index 00000000..9af71eb4 --- /dev/null +++ b/NativeScript/ffi/shared/jsi/NativeApiJsiInstall.h @@ -0,0 +1,1567 @@ +Object CreateNativeApiJSI(Runtime& runtime, const NativeApiJsiConfig& config) { + auto bridge = std::make_shared(config); + return Object::createFromHostObject( + runtime, std::make_shared(std::move(bridge))); +} + +void NativeApiJsiWriteSmokeStage(const char* stage) { + const char* enabled = getenv("NATIVESCRIPT_RN_TURBO_SMOKE_MARKER"); + if (enabled == nullptr || enabled[0] == '\0') { + return; + } + + NSString* path = [NSTemporaryDirectory() + stringByAppendingPathComponent:@"NativeScriptNativeApiSmoke.marker"]; + NSString* content = + [NSString stringWithFormat:@"stage=%s\n", stage != nullptr ? stage : ""]; + [content writeToFile:path atomically:YES encoding:NSUTF8StringEncoding error:nil]; +} + +void InstallAggregateGlobals(Runtime& runtime, Object& api, const char* namesFunction) { + Value metadataValue = api.getProperty(runtime, "metadata"); + if (!metadataValue.isObject()) { + return; + } + Object metadata = metadataValue.asObject(runtime); + Value namesValue = metadata.getProperty(runtime, namesFunction); + if (!namesValue.isObject()) { + return; + } + Object namesObject = namesValue.asObject(runtime); + if (!namesObject.isFunction(runtime)) { + return; + } + Value namesResult = namesObject.asFunction(runtime).call(runtime); + if (!namesResult.isObject() || !namesResult.asObject(runtime).isArray(runtime)) { + return; + } + Array names = namesResult.asObject(runtime).getArray(runtime); + Object global = runtime.global(); + for (size_t i = 0; i < names.size(runtime); i++) { + Value nameValue = names.getValueAtIndex(runtime, i); + if (!nameValue.isString()) { + continue; + } + std::string name = nameValue.asString(runtime).utf8(runtime); + if (name.empty() || global.hasProperty(runtime, name.c_str())) { + continue; + } + try { + Value aggregate = api.getProperty(runtime, name.c_str()); + if (!aggregate.isUndefined()) { + global.setProperty(runtime, name.c_str(), aggregate); + } + } catch (const std::exception&) { + // Some React Native globals are read-only even when hasProperty misses + // them. Keep NativeScript initialization resilient and skip collisions. + } + } +} + +std::string jsStringLiteral(const char* value) { + std::string result = "'"; + if (value != nullptr) { + for (const char* current = value; *current != '\0'; current++) { + switch (*current) { + case '\\': + result += "\\\\"; + break; + case '\'': + result += "\\'"; + break; + case '\n': + result += "\\n"; + break; + case '\r': + result += "\\r"; + break; + case '\t': + result += "\\t"; + break; + default: + result += *current; + break; + } + } + } + result += "'"; + return result; +} + +void InstallNativeApiJsiGlobalSymbols(Runtime& runtime, const char* globalName) { + NativeApiJsiWriteSmokeStage("jsi:globals:before-eval"); + static const char* GlobalInstaller = R"JSI_GLOBALS( +(function(nativeApiGlobalName) { + 'use strict'; + var api = globalThis[nativeApiGlobalName]; + var installedFlagName = '__nativeScriptNativeApiGlobalsInstalled'; + if (!api || globalThis[installedFlagName]) { + return; + } + + var cacheName = '__nativeScriptNativeApiGlobalCache'; + var typeCodeKey = '__nativeApiTypeCode'; + var classWrappers = typeof WeakMap === 'function' ? new WeakMap() : null; + var classWrappersByName = Object.create(null); + var resolvingGlobal = Object.create(null); + + function globalCache() { + var existing = globalThis[cacheName]; + if (existing && typeof existing === 'object') { + return existing; + } + var cache = Object.create(null); + Object.defineProperty(globalThis, cacheName, { + configurable: false, + enumerable: false, + writable: false, + value: cache + }); + return cache; + } + + function cacheGlobal(name, value) { + if (name && value !== undefined) { + globalCache()[name] = value; + } + } + + function resolveCachedGlobal(name, expectedKind) { + if (!name) { + return undefined; + } + var cached = globalCache()[name]; + if (cached && (typeof cached === 'object' || typeof cached === 'function') && cached.kind === expectedKind) { + return cached; + } + if (resolvingGlobal[name] || !Object.prototype.hasOwnProperty.call(globalThis, name)) { + return undefined; + } + resolvingGlobal[name] = true; + try { + var value = globalThis[name]; + if (value && (typeof value === 'object' || typeof value === 'function') && value.kind === expectedKind) { + cacheGlobal(name, value); + return value; + } + } finally { + delete resolvingGlobal[name]; + } + return undefined; + } + + function defineLazyGlobal(name, resolve, force, nativeKind) { + if (!name) { + return; + } + if (!force && Object.prototype.hasOwnProperty.call(globalThis, name)) { + try { + cacheGlobal(name, globalThis[name]); + } catch (_) { + } + return; + } + var nativeDefineLazyGlobal = api.__defineLazyGlobal; + if (nativeKind && typeof nativeDefineLazyGlobal === 'function' && + typeof globalThis.__nativeScriptResolveNativeApiLazyGlobal === 'function') { + try { + if (nativeDefineLazyGlobal(name, nativeKind, !!force)) { + return; + } + } catch (_) { + } + } + try { + Object.defineProperty(globalThis, name, { + configurable: true, + enumerable: false, + get: function() { + var value = resolve(name); + cacheGlobal(name, value); + Object.defineProperty(globalThis, name, { + configurable: true, + enumerable: false, + writable: false, + value: value + }); + return value; + } + }); + } catch (_) { + var value = resolve(name); + if (value !== undefined) { + cacheGlobal(name, value); + Object.defineProperty(globalThis, name, { + configurable: true, + enumerable: false, + writable: false, + value: value + }); + } + } + } + + Object.defineProperty(globalThis, '__nativeScriptResolveNativeApiGlobal', { + configurable: false, + enumerable: false, + writable: false, + value: resolveCachedGlobal + }); + + Object.defineProperty(globalThis, '__nativeScriptResolveNativeApiClassWrapper', { + configurable: false, + enumerable: false, + writable: false, + value: function(name) { + return name ? classWrappersByName[name] : undefined; + } + }); + + function findPrototypeDescriptor(className, property) { + var prototype; + if (className && (typeof className === 'object' || typeof className === 'function')) { + prototype = className; + } else { + var wrapper = className ? classWrappersByName[className] : undefined; + prototype = wrapper && wrapper.prototype; + } + while (prototype != null) { + var descriptor = Object.getOwnPropertyDescriptor(prototype, property); + if (descriptor) { + return descriptor; + } + prototype = Object.getPrototypeOf(prototype); + } + return undefined; + } + + Object.defineProperty(globalThis, '__nativeScriptGetNativeApiPrototypeProperty', { + configurable: false, + enumerable: false, + writable: false, + value: function(className, receiver, property) { + var descriptor = findPrototypeDescriptor(className, property); + if (!descriptor) { + return { found: false }; + } + if (typeof descriptor.get === 'function') { + return { found: true, value: descriptor.get.call(receiver) }; + } + if (typeof descriptor.value === 'function') { + return { found: true, value: descriptor.value.bind(receiver) }; + } + if ('value' in descriptor) { + return { found: true, value: descriptor.value }; + } + return { found: true, value: undefined }; + } + }); + + Object.defineProperty(globalThis, '__nativeScriptCreateNativeApiIterator', { + configurable: false, + enumerable: false, + writable: false, + value: function(receiver, prototype) { + if (!receiver || typeof Symbol !== 'function') { + return undefined; + } + var descriptor = findPrototypeDescriptor(prototype || receiver.className, Symbol.iterator); + if (descriptor && typeof descriptor.value === 'function') { + return descriptor.value.call(receiver); + } + if (descriptor && typeof descriptor.get === 'function') { + var getterValue = descriptor.get.call(receiver); + if (typeof getterValue === 'function') { + return getterValue.call(receiver); + } + } + var iteratorMethod = receiver[Symbol.iterator]; + return typeof iteratorMethod === 'function' + ? iteratorMethod.call(receiver) + : undefined; + } + }); + + function wrapAggregateConstructor(nativeConstructor) { + if (typeof nativeConstructor !== 'function') { + return nativeConstructor; + } + var aggregate = function NativeScriptAggregate(initialValue) { + return nativeConstructor(initialValue); + }; + try { + Object.defineProperty(aggregate, Symbol.hasInstance, { + configurable: true, + enumerable: false, + value: function(value) { + return !!value && + typeof value === 'object' && + value.kind === nativeConstructor.kind && + value.name === nativeConstructor.runtimeName; + } + }); + } catch (_) { + } + ['kind', 'runtimeName', 'metadataOffset', 'sizeof', 'fields', 'equals'].forEach(function(key) { + try { + Object.defineProperty(aggregate, key, { + configurable: true, + enumerable: false, + writable: false, + value: nativeConstructor[key] + }); + } catch (_) { + } + }); + return aggregate; + } + + function setDescriptorValue(target, property, receiver, value) { + var descriptor = Object.getOwnPropertyDescriptor(target, property); + if (!descriptor) { + return false; + } + if (typeof descriptor.set === 'function') { + descriptor.set.call(receiver, value); + return true; + } + if (descriptor.writable) { + if (receiver && receiver !== target) { + Object.defineProperty(receiver, property, { + configurable: true, + enumerable: true, + writable: true, + value: value + }); + } else { + target[property] = value; + } + return true; + } + return false; + } + + function isConstructorOptions(value) { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + return false; + } + if (value.kind || value.nativeAddress || value instanceof Date) { + return false; + } + return Object.getPrototypeOf(value) === Object.prototype || + Object.getPrototypeOf(value) === null; + } + + function capitalizeToken(value) { + value = String(value || ''); + return value ? value.charAt(0).toUpperCase() + value.slice(1) : value; + } + + function selectorCandidatesFromOptions(options) { + var keys = Object.keys(options || {}); + if (!keys.length) { + return []; + } + var first = capitalizeToken(keys[0]); + var tail = ''; + for (var i = 1; i < keys.length; i++) { + tail += keys[i] + ':'; + } + return [ + 'initWith' + first + ':' + tail, + 'init' + first + ':' + tail + ]; + } + + function valuesFromOptions(options) { + return Object.keys(options || {}).map(function(key) { + return options[key]; + }); + } + + function selectorScoreForArguments(selectorName, args) { + if (!selectorName || selectorName.indexOf('init') !== 0) { + return -1; + } + if (selectorName === 'init') { + return args.length === 0 ? 100 : -1; + } + if (args.length === 0) { + return -1; + } + var lower = selectorName.toLowerCase(); + var first = args[0]; + var score = 1; + if (Array.isArray(first)) { + if (lower.indexOf('array') !== -1) { + score += 40; + } + } else if (typeof first === 'string') { + if (lower.indexOf('string') !== -1) { + score += 40; + } + if (lower.indexOf('url') !== -1) { + score += 10; + } + } else if (typeof first === 'number') { + if (lower.indexOf('primitive') !== -1) { + score += 50; + } + if (lower.indexOf('int') !== -1 || + lower.indexOf('integer') !== -1 || + lower.indexOf('number') !== -1 || + lower.indexOf('float') !== -1 || + lower.indexOf('double') !== -1 || + lower.indexOf('long') !== -1 || + lower.indexOf('short') !== -1) { + score += 30; + } + } else if (isConstructorOptions(first)) { + if (lower.indexOf('struct') !== -1 || + lower.indexOf('structure') !== -1) { + score += 40; + } + if (lower.indexOf('dictionary') !== -1) { + score += 20; + } + } else if (first === null || typeof first === 'undefined') { + score += 5; + } else if (lower.indexOf('object') !== -1 || + lower.indexOf('url') !== -1 || + lower.indexOf('data') !== -1) { + score += 20; + } + + var allStrings = args.length > 1 && args.every(function(value) { + return typeof value === 'string'; + }); + var allNumbers = args.length > 1 && args.every(function(value) { + return typeof value === 'number'; + }); + if (allStrings && lower.indexOf('string') !== -1) { + score += 25; + } + if (allNumbers && + (lower.indexOf('int') !== -1 || lower.indexOf('number') !== -1)) { + score += 25; + } + return score; + } + + function initializerMembers(nativeClass, argumentCount) { + var members = nativeClass.__instanceMembers || []; + var result = []; + for (var i = 0; i < members.length; i++) { + var member = members[i]; + if (!member || member.property || !member.selectorName) { + continue; + } + if (member.selectorName.indexOf('init') !== 0) { + continue; + } + if (typeof argumentCount === 'number' && + member.argumentCount !== argumentCount) { + continue; + } + result.push(member); + } + return result; + } + + function chooseInitializer(nativeClass, args, optionSelectors) { + var members = initializerMembers(nativeClass, args.length); + if (!members.length) { + return null; + } + if (optionSelectors && optionSelectors.length) { + for (var i = 0; i < optionSelectors.length; i++) { + for (var j = 0; j < members.length; j++) { + if (members[j].selectorName === optionSelectors[i]) { + return members[j]; + } + } + } + } + + var best = null; + var bestScore = -1; + for (var k = 0; k < members.length; k++) { + var score = selectorScoreForArguments(members[k].selectorName, args); + if (score > bestScore) { + bestScore = score; + best = members[k]; + } + } + return bestScore >= 0 ? best : null; + } + + function chooseInitializerBySelectors(nativeClass, args, selectors) { + if (!selectors || !selectors.length) { + return null; + } + var members = initializerMembers(nativeClass, args.length); + for (var i = 0; i < selectors.length; i++) { + for (var j = 0; j < members.length; j++) { + if (members[j].selectorName === selectors[i]) { + return members[j]; + } + } + } + return null; + } + + function unavailableInitializerError(error) { + return error && + /Objective-C selector is not available/.test(String(error.message || error)); + } + + function constructNativeInstance(nativeClass, args) { + if (args.length === 1 && + args[0] && + typeof args[0] === 'object' && + (args[0].kind === 'pointer' || args[0].kind === 'reference') && + typeof nativeClass.construct === 'function') { + return nativeClass.construct(args[0]); + } + + var actualArgs = args; + var initializer = null; + if (args.length === 1 && isConstructorOptions(args[0])) { + var optionSelectors = selectorCandidatesFromOptions(args[0]); + if (!optionSelectors.length) { + throw new Error('No initializer found that matches constructor invocation.'); + } + var optionArgs = valuesFromOptions(args[0]); + initializer = chooseInitializerBySelectors( + nativeClass, + optionArgs, + optionSelectors + ); + if (initializer) { + actualArgs = optionArgs; + } + } + if (!initializer) { + initializer = chooseInitializer(nativeClass, actualArgs, null); + } + if (!initializer) { + throw new Error('No initializer found that matches constructor invocation.'); + } + if (typeof nativeClass.alloc !== 'function') { + throw new Error('Native class cannot be allocated'); + } + var instance = nativeClass.alloc(); + if (initializer.selectorName === 'init') { + if (typeof instance.init !== 'function') { + throw new Error('No initializer found that matches constructor invocation.'); + } + return instance.init(); + } + try { + if (initializer.name && typeof instance[initializer.name] === 'function') { + return instance[initializer.name].apply(instance, actualArgs); + } + var invokeArgs = [initializer.selectorName]; + Array.prototype.push.apply(invokeArgs, actualArgs); + return instance.invoke.apply(instance, invokeArgs); + } catch (error) { + if (unavailableInitializerError(error)) { + throw new Error('No initializer found that matches constructor invocation.'); + } + throw error; + } + } + + function wrapNativeClass(nativeClass) { + if (!nativeClass || (typeof nativeClass !== 'object' && typeof nativeClass !== 'function')) { + return nativeClass; + } + var nativeClassName = nativeClass.runtimeName || nativeClass.name || ''; + if (nativeClassName && classWrappersByName[nativeClassName]) { + if (classWrappers) { + try { + classWrappers.set(nativeClass, classWrappersByName[nativeClassName]); + } catch (_) { + } + } + return classWrappersByName[nativeClassName]; + } + if (classWrappers) { + var cached = classWrappers.get(nativeClass); + if (cached) { + return cached; + } + } + var constructable = function NativeScriptNativeClass() { + var args = Array.prototype.slice.call(arguments); + var redirectConstructor = this && this.constructor; + if (redirectConstructor && + redirectConstructor !== constructable && + redirectConstructor !== wrapper && + typeof redirectConstructor.__nativeApiEnsureClass === 'function') { + var redirectedWrapper = redirectConstructor.__nativeApiEnsureClass(); + if (redirectedWrapper && + redirectedWrapper !== constructable && + redirectedWrapper !== wrapper && + typeof redirectedWrapper.apply === 'function') { + return rememberClassOnInstance( + redirectedWrapper.apply(this, args), + redirectConstructor + ); + } + } + if (args.length > 0) { + return rememberInstanceClass(constructNativeInstance(nativeClass, args)); + } + if (typeof nativeClass.alloc !== 'function') { + throw new Error('Native class cannot be allocated'); + } + var instance = nativeClass.alloc(); + if (instance && typeof instance.init === 'function') { + return rememberInstanceClass(instance.init()); + } + return rememberInstanceClass(instance); + }; + function rememberInstanceClass(instance) { + return rememberClassOnInstance(instance, wrapper || constructable); + } + try { + Object.defineProperty(constructable, 'name', { + configurable: true, + enumerable: false, + value: nativeClassName || nativeClass.name || 'NativeScriptNativeClass' + }); + } catch (_) { + } + try { + Object.defineProperty(constructable, 'extend', { + configurable: true, + enumerable: false, + writable: false, + value: function(methods, options) { + if (methods == null || typeof methods !== 'object') { + throw new Error('extend() first parameter must be an object'); + } + var extendOptions = options || {}; + if (typeof Symbol === 'function' && + Object.prototype.hasOwnProperty.call(methods, Symbol.iterator)) { + try { + extendOptions = Object.assign({}, extendOptions, { + __hasIterator: true + }); + } catch (_) { + extendOptions.__hasIterator = true; + } + } + var extendedNativeClass = api.__extendClass(nativeClass, methods, extendOptions); + var extended = wrapNativeClass(extendedNativeClass); + try { + Object.setPrototypeOf(extended, wrapper || constructable); + } catch (_) { + } + var extendedPrototype = Object.create(constructable.prototype || null); + try { + Object.defineProperties(extendedPrototype, Object.getOwnPropertyDescriptors(methods)); + } catch (_) { + Object.keys(methods).forEach(function(key) { + extendedPrototype[key] = methods[key]; + }); + } + try { + Object.defineProperty(extendedPrototype, 'constructor', { + configurable: true, + enumerable: false, + writable: true, + value: extended + }); + } catch (_) { + } + extended.prototype = extendedPrototype; + try { + api.__rememberClassWrapper(extendedNativeClass, extended, extendedPrototype); + } catch (_) { + } + return extended; + } + }); + } catch (_) { + } + try { + Object.defineProperty(constructable, 'alloc', { + configurable: true, + enumerable: false, + writable: true, + value: function() { + return rememberInstanceClass(nativeClass.alloc.apply(nativeClass, arguments)); + } + }); + } catch (_) { + } + try { + Object.defineProperty(constructable, 'caller', { + configurable: true, + enumerable: false, + writable: false, + value: null + }); + } catch (_) { + } + try { + Object.defineProperty(constructable, 'arguments', { + configurable: true, + enumerable: false, + writable: false, + value: null + }); + } catch (_) { + } + var basePrototypeTarget = {}; + function installClassMembers(target, members, receiverIsClass) { + if (!target || !members || typeof members.length !== 'number') { + return; + } + for (var i = 0; i < members.length; i++) { + var member = members[i]; + if (!member || !member.name || Object.prototype.hasOwnProperty.call(target, member.name)) { + continue; + } + try { + if (member.property) { + var descriptor = { + configurable: true, + enumerable: false, + get: receiverIsClass + ? (function(name, selectorName) { + return function() { + return selectorName + ? nativeClass.invoke(selectorName) + : nativeClass[name]; + }; + })(member.name, member.selectorName) + : (function(name) { + return function() { + return api.__invokeBase(nativeClass, this, name); + }; + })(member.name) + }; + if (!member.readonly) { + descriptor.set = receiverIsClass + ? (function(name, setterSelectorName) { + return function(value) { + if (setterSelectorName) { + return nativeClass.invoke(setterSelectorName, value); + } + nativeClass[name] = value; + }; + })(member.name, member.setterSelectorName) + : (function(name) { + return function(value) { + return api.__invokeBase(nativeClass, this, name, value); + }; + })(member.name); + } + Object.defineProperty(target, member.name, descriptor); + } else { + Object.defineProperty(target, member.name, { + configurable: true, + enumerable: false, + writable: true, + value: receiverIsClass + ? (function(name) { + return function() { + if (this && typeof this === 'object' && this.kind === 'object') { + var baseArgs = [nativeClass, this, name]; + Array.prototype.push.apply(baseArgs, arguments); + return api.__invokeBase.apply(api, baseArgs); + } + return nativeClass[name].apply(nativeClass, arguments); + }; + })(member.name) + : (function(name) { + return function() { + var args = [nativeClass, this, name]; + Array.prototype.push.apply(args, arguments); + return api.__invokeBase.apply(api, args); + }; + })(member.name) + }); + } + } catch (_) { + } + } + } + installClassMembers(constructable, nativeClass.__staticMembers, true); + installClassMembers(basePrototypeTarget, nativeClass.__instanceMembers, false); + try { + Object.defineProperty(basePrototypeTarget, 'constructor', { + configurable: true, + enumerable: false, + writable: true, + value: constructable + }); + } catch (_) { + } + try { + Object.defineProperty(basePrototypeTarget, 'toString', { + configurable: true, + enumerable: false, + writable: true, + value: function() { + return '[object NativeScriptObject]'; + } + }); + } catch (_) { + } + try { + if (typeof Symbol === 'function' && Symbol.iterator && + typeof api.__fastEnumeration === 'function') { + Object.defineProperty(basePrototypeTarget, Symbol.iterator, { + configurable: true, + enumerable: false, + writable: true, + value: function() { + return api.__fastEnumeration(this); + } + }); + } + } catch (_) { + } + constructable.prototype = typeof Proxy === 'function' + ? new Proxy(basePrototypeTarget, { + get: function(target, property, receiver) { + if (property in target) { + return Reflect.get(target, property, receiver); + } + if (typeof property === 'symbol') { + return undefined; + } + return function() { + var args = [nativeClass, this, String(property)]; + Array.prototype.push.apply(args, arguments); + return api.__invokeBase.apply(api, args); + }; + }, + set: function(target, property, value, receiver) { + if (property === 'prototype') { + target[property] = value; + return true; + } + if (setDescriptorValue(target, property, receiver, value)) { + return true; + } + if (receiver && receiver !== target) { + Object.defineProperty(receiver, property, { + configurable: true, + enumerable: true, + writable: true, + value: value + }); + return true; + } + target[property] = value; + return true; + }, + has: function(target, property) { + return property in target; + } + }) + : basePrototypeTarget; + try { + Object.defineProperty(constructable, Symbol.hasInstance, { + configurable: true, + enumerable: false, + value: function(value) { + if (!value || typeof value !== 'object') { + return false; + } + var expectedName = nativeClass.runtimeName || nativeClass.name; + try { + if (typeof value.isKindOfClass === 'function' && + value.isKindOfClass(constructable)) { + return true; + } + } catch (_) { + } + try { + var current = typeof value.class === 'function' ? value.class() : null; + while (current) { + if (current === wrapper || current === constructable) { + return true; + } + var currentName = current.runtimeName || current.name; + if (typeof expectedName === 'string' && currentName === expectedName) { + return true; + } + var next = current.superclass || null; + if (typeof next === 'function' && next.kind !== 'class') { + next = next.call(current); + } + current = next || null; + } + } catch (_) { + } + return typeof expectedName === 'string' && value.className === expectedName; + } + }); + } catch (_) { + } + var wrapper = typeof Proxy === 'function' + ? new Proxy(constructable, { + get: function(target, property, receiver) { + if (property === '__nativeApiClass') { + return nativeClass; + } + if (Object.prototype.hasOwnProperty.call(target, property) || + property === 'prototype' || + property === 'length' || + property === 'name') { + return Reflect.get(target, property, receiver); + } + var nativeValue = nativeClass[property]; + if (nativeValue !== undefined) { + return nativeValue; + } + var reflected = Reflect.get(target, property, receiver); + if (reflected !== undefined || property in target) { + return reflected; + } + return reflected; + }, + set: function(target, property, value, receiver) { + if (property === 'prototype') { + target[property] = value; + return true; + } + if (setDescriptorValue(target, property, receiver, value)) { + return true; + } + try { + nativeClass[property] = value; + return true; + } catch (_) { + } + if (receiver && receiver !== target) { + Object.defineProperty(receiver, property, { + configurable: true, + enumerable: true, + writable: true, + value: value + }); + return true; + } + return Reflect.set(target, property, value, receiver); + }, + has: function(target, property) { + return property in target || property in nativeClass; + } + }) + : constructable; + try { + var nativeSuperclass = nativeClass.__superclass; + if (nativeSuperclass && nativeSuperclass !== nativeClass) { + var superclassWrapper = wrapNativeClass(nativeSuperclass); + if (superclassWrapper && + superclassWrapper !== wrapper && + superclassWrapper !== constructable) { + Object.setPrototypeOf(wrapper, superclassWrapper); + } + } + } catch (_) { + } + if (classWrappers) { + classWrappers.set(nativeClass, wrapper); + } + try { + api.__rememberClassWrapper(nativeClass, wrapper, constructable.prototype); + } catch (_) { + } + if (nativeClassName) { + classWrappersByName[nativeClassName] = wrapper; + cacheGlobal(nativeClassName, wrapper); + if (!Object.prototype.hasOwnProperty.call(globalThis, nativeClassName)) { + try { + Object.defineProperty(globalThis, nativeClassName, { + configurable: true, + enumerable: false, + writable: false, + value: wrapper + }); + } catch (_) { + } + } + } + if (nativeClass.name && nativeClass.name !== nativeClassName) { + classWrappersByName[nativeClass.name] = wrapper; + cacheGlobal(nativeClass.name, wrapper); + } + return wrapper; + } + + function rememberClassOnInstance(instance, classWrapper) { + if (instance && typeof instance === 'object' && classWrapper) { + try { + instance.__nativeApiClassWrapper = classWrapper; + } catch (_) { + } + } + return instance; + } + + function isNativeClassLike(value) { + if (!value || (typeof value !== 'object' && typeof value !== 'function')) { + return false; + } + if (value.kind === 'class') { + return true; + } + try { + return !!value.__nativeApiClass; + } catch (_) { + return false; + } + } + + function nativeClassLikeHandle(value) { + if (!value || (typeof value !== 'object' && typeof value !== 'function')) { + return value; + } + try { + if (typeof value.__nativeApiEnsureClass === 'function') { + value = value.__nativeApiEnsureClass(); + } + } catch (_) { + } + try { + return value.__nativeApiClass || value; + } catch (_) { + return value; + } + } + + function materializeTypeScriptNativeClass(constructor) { + if (!constructor || typeof constructor !== 'function') { + return undefined; + } + var state = constructor.__nativeApiTypeScriptState; + if (!state) { + return undefined; + } + if (state.wrapper) { + return state.wrapper; + } + if (state.materializing) { + return state.base; + } + + state.materializing = true; + try { + var baseWrapper = state.base; + if (baseWrapper && typeof baseWrapper.__nativeApiEnsureClass === 'function') { + baseWrapper = baseWrapper.__nativeApiEnsureClass(); + } + + var options = {}; + var className = constructor.ObjCClassName || constructor.name; + if (className) { + options.name = className; + } + if (constructor.ObjCProtocols) { + options.protocols = constructor.ObjCProtocols; + } + if (constructor.ObjCExposedMethods) { + options.exposedMethods = constructor.ObjCExposedMethods; + } + + var nativeBase = nativeClassLikeHandle(baseWrapper); + var nativeClass = api.__extendClass(nativeBase, constructor.prototype || {}, options); + var wrapper = wrapNativeClass(nativeClass); + state.wrapper = wrapper; + + try { + Object.setPrototypeOf(constructor, wrapper); + } catch (_) { + } + try { + api.__rememberClassWrapper(nativeClass, constructor, constructor.prototype || {}); + } catch (_) { + } + return wrapper; + } finally { + state.materializing = false; + } + } + + function defineTypeScriptStaticForwarder(constructor, name, isProperty, readonly) { + if (!name || name === 'length' || name === 'name' || name === 'prototype' || + Object.prototype.hasOwnProperty.call(constructor, name)) { + return; + } + + var descriptor = { + configurable: true, + enumerable: false + }; + + if (isProperty) { + descriptor.get = function() { + var wrapper = materializeTypeScriptNativeClass(constructor); + return wrapper ? wrapper[name] : undefined; + }; + if (!readonly) { + descriptor.set = function(value) { + var wrapper = materializeTypeScriptNativeClass(constructor); + if (wrapper) { + wrapper[name] = value; + } + }; + } + } else { + descriptor.writable = true; + descriptor.value = function() { + if (name === 'class') { + materializeTypeScriptNativeClass(constructor); + return constructor; + } + if (name === 'superclass') { + var state = constructor.__nativeApiTypeScriptState; + return state && state.base; + } + var wrapper = materializeTypeScriptNativeClass(constructor); + var member = wrapper && wrapper[name]; + if (typeof member !== 'function') { + throw new TypeError(String(name) + ' is not a function'); + } + var result = member.apply(wrapper, arguments); + if (name === 'alloc' || name === 'new' || name === 'construct') { + return rememberClassOnInstance(result, constructor); + } + return result; + }; + } + + try { + Object.defineProperty(constructor, name, descriptor); + } catch (_) { + } + } + + function installTypeScriptNativeClassSupport(constructor, base) { + if (!constructor || typeof constructor !== 'function' || !isNativeClassLike(base)) { + return false; + } + if (constructor.__nativeApiTypeScriptState) { + return true; + } + + try { + Object.defineProperty(constructor, '__nativeApiTypeScriptState', { + configurable: false, + enumerable: false, + writable: false, + value: { + base: base, + wrapper: null, + materializing: false + } + }); + } catch (_) { + constructor.__nativeApiTypeScriptState = { + base: base, + wrapper: null, + materializing: false + }; + } + + try { + Object.defineProperty(constructor, '__nativeApiEnsureClass', { + configurable: false, + enumerable: false, + writable: false, + value: function() { + return materializeTypeScriptNativeClass(constructor); + } + }); + } catch (_) { + } + + try { + Object.defineProperty(constructor, '__nativeApiClass', { + configurable: true, + enumerable: false, + get: function() { + var wrapper = materializeTypeScriptNativeClass(constructor); + return wrapper && wrapper.__nativeApiClass; + } + }); + } catch (_) { + } + + ['alloc', 'new', 'class', 'superclass', 'extend'].forEach(function(name) { + defineTypeScriptStaticForwarder(constructor, name, false, false); + }); + + try { + var members = base.__staticMembers || []; + for (var i = 0; i < members.length; i++) { + var member = members[i]; + if (member && member.name) { + defineTypeScriptStaticForwarder( + constructor, + member.name, + !!member.property, + !!member.readonly + ); + } + } + } catch (_) { + } + + return true; + } + + function installTypeScriptNativeHelpers() { + var extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function(d, b) { d.__proto__ = b; }) || + function(d, b) { + for (var p in b) { + if (Object.prototype.hasOwnProperty.call(b, p)) { + d[p] = b[p]; + } + } + }; + + globalThis.__extends = function(d, b) { + if (typeof b !== 'function' && b !== null) { + throw new TypeError('Class extends value ' + String(b) + ' is not a constructor or null'); + } + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + if (b !== null) { + installTypeScriptNativeClassSupport(d, b); + } + }; + + globalThis.NativeClass = function NativeClass(constructor) { + if (constructor && typeof constructor.__nativeApiEnsureClass === 'function') { + constructor.__nativeApiEnsureClass(); + } + return constructor; + }; + + globalThis.ObjCClass = function ObjCClass() { + var protocols = Array.prototype.slice.call(arguments); + return function(constructor) { + if (constructor.ObjCProtocols) { + Array.prototype.push.apply(constructor.ObjCProtocols, protocols); + } else { + constructor.ObjCProtocols = protocols; + } + if (typeof constructor.__nativeApiEnsureClass === 'function') { + constructor.__nativeApiEnsureClass(); + } + return constructor; + }; + }; + } + + function wrapInteropFactory(nativeFactory, properties) { + if (typeof nativeFactory !== 'function' || nativeFactory.__nativeScriptConstructable) { + return nativeFactory; + } + var constructable = function NativeScriptInteropValue() { + return nativeFactory.apply(undefined, arguments); + }; + try { + if (nativeFactory.prototype) { + constructable.prototype = nativeFactory.prototype; + } + } catch (_) { + } + try { + Object.defineProperty(constructable, Symbol.hasInstance, { + configurable: true, + enumerable: false, + value: function(value) { + return !!value && typeof value === 'object' && value.kind === properties.kind; + } + }); + } catch (_) { + } + Object.keys(properties).forEach(function(key) { + try { + Object.defineProperty(constructable, key, { + configurable: true, + enumerable: false, + writable: false, + value: properties[key] + }); + } catch (_) { + } + }); + Object.defineProperty(constructable, '__nativeScriptConstructable', { + configurable: false, + enumerable: false, + writable: false, + value: true + }); + return constructable; + } + + function installInteropConstructors() { + var interop = globalThis.interop; + if (!interop || typeof interop !== 'object') { + return; + } + var pointerSize; + try { + if (typeof interop.sizeof === 'function' && interop.types && interop.types.pointer !== undefined) { + pointerSize = interop.sizeof(interop.types.pointer); + } + } catch (_) { + pointerSize = undefined; + } + interop.Pointer = wrapInteropFactory(interop.Pointer, { kind: 'pointer', sizeof: pointerSize }); + interop.Reference = wrapInteropFactory(interop.Reference, { kind: 'reference', sizeof: pointerSize }); + interop.FunctionReference = wrapInteropFactory( + interop.FunctionReference, + { kind: 'functionReference', sizeof: pointerSize } + ); + if (interop.types && typeof interop.types === 'object') { + Object.keys(interop.types).forEach(function(name) { + var value = interop.types[name]; + if (typeof value !== 'number') { + return; + } + var boxed = { + valueOf: function() { return value; }, + toString: function() { return String(value); } + }; + Object.defineProperty(boxed, typeCodeKey, { + configurable: false, + enumerable: false, + writable: false, + value: value + }); + interop.types[name] = boxed; + }); + } + } + + function defineInlineFunction(name, value) { + if (Object.prototype.hasOwnProperty.call(globalThis, name)) { + return; + } + Object.defineProperty(globalThis, name, { + configurable: true, + enumerable: false, + writable: true, + value: value + }); + } + + function installInlineFunctions() { + var makePoint = function(x, y) { return { x: x, y: y }; }; + var makeSize = function(width, height) { return { width: width, height: height }; }; + var makeRect = function(x, y, width, height) { + return { origin: { x: x, y: y }, size: { width: width, height: height } }; + }; + defineInlineFunction('CGPointMake', makePoint); + defineInlineFunction('NSMakePoint', makePoint); + defineInlineFunction('CGSizeMake', makeSize); + defineInlineFunction('NSMakeSize', makeSize); + defineInlineFunction('CGRectMake', makeRect); + defineInlineFunction('NSMakeRect', makeRect); + defineInlineFunction('NSMakeRange', function(location, length) { + return { location: location, length: length }; + }); + defineInlineFunction('UIEdgeInsetsMake', function(top, left, bottom, right) { + return { top: top, left: left, bottom: bottom, right: right }; + }); + } + + function names(kind) { + var metadata = api.metadata; + var fn = metadata && metadata[kind]; + return typeof fn === 'function' ? fn() : []; + } + + function nameSet(values) { + var result = Object.create(null); + (values || []).forEach(function(value) { + result[value] = true; + }); + return result; + } + + var classNameList = names('classNames'); + var functionNameList = names('functionNames'); + var constantNameList = names('constantNames'); + var protocolNameList = names('protocolNames'); + var enumNameList = names('enumNames'); + var functionNameSet = nameSet(functionNameList); + var constantNameSet = nameSet(constantNameList); + var classNameSet = nameSet(classNameList); + var protocolNameSet = nameSet(protocolNameList); + var enumNameSet = nameSet(enumNameList); + + function resolveNativeApiEnum(enumName) { + return (api.getEnum && api.getEnum(enumName)) || api[enumName]; + } + + Object.defineProperty(globalThis, '__nativeScriptResolveNativeApiLazyGlobal', { + configurable: false, + enumerable: false, + writable: false, + value: function(name, kind) { + var value; + if (kind === 'class') { + value = wrapNativeClass(api[name]); + } else if (kind === 'function' || kind === 'constant') { + value = api[name]; + } else if (kind === 'protocol') { + value = (api.getProtocol && api.getProtocol(name)) || api[name]; + } else if (kind === 'enum') { + value = resolveNativeApiEnum(name); + } else if (kind === 'struct') { + value = wrapAggregateConstructor((api.getStruct && api.getStruct(name)) || api[name]); + } else if (kind === 'union') { + value = wrapAggregateConstructor((api.getUnion && api.getUnion(name)) || api[name]); + } else if (kind && kind.indexOf('enumMember:') === 0) { + var enumValue = resolveNativeApiEnum(kind.slice('enumMember:'.length)); + value = enumValue && enumValue[name]; + } else { + value = api[name]; + } + cacheGlobal(name, value); + return value; + } + }); + + classNameList.forEach(function(name) { + defineLazyGlobal(name, function(className) { + return wrapNativeClass(api[className]); + }, false, 'class'); + }); + functionNameList.forEach(function(name) { + defineLazyGlobal(name, function(functionName) { + return api[functionName]; + }, false, 'function'); + }); + constantNameList.forEach(function(name) { + defineLazyGlobal(name, function(constantName) { + return api[constantName]; + }, false, 'constant'); + }); + protocolNameList.forEach(function(name) { + defineLazyGlobal(name, function(protocolName) { + return (api.getProtocol && api.getProtocol(protocolName)) || api[protocolName]; + }, false, 'protocol'); + }); + enumNameList.forEach(function(name) { + defineLazyGlobal(name, resolveNativeApiEnum, false, 'enum'); + var enumValue = resolveNativeApiEnum(name); + if (!enumValue || typeof enumValue !== 'object') { + return; + } + Object.keys(enumValue).forEach(function(memberName) { + if (/^-?\d+$/.test(memberName)) { + return; + } + defineLazyGlobal(memberName, function() { + return enumValue[memberName]; + }, false, 'enumMember:' + name); + }); + }); + names('structNames').forEach(function(name) { + var conflictsWithValue = + !!functionNameSet[name] || !!constantNameSet[name] || !!classNameSet[name] || + !!protocolNameSet[name] || !!enumNameSet[name]; + defineLazyGlobal(name, function(structName) { + return wrapAggregateConstructor((api.getStruct && api.getStruct(structName)) || api[structName]); + }, !conflictsWithValue, 'struct'); + }); + names('unionNames').forEach(function(name) { + var conflictsWithValue = + !!functionNameSet[name] || !!constantNameSet[name] || !!classNameSet[name] || + !!protocolNameSet[name] || !!enumNameSet[name]; + defineLazyGlobal(name, function(unionName) { + return wrapAggregateConstructor((api.getUnion && api.getUnion(unionName)) || api[unionName]); + }, !conflictsWithValue, 'union'); + }); + + if (typeof globalThis.UIColor === 'undefined' && + typeof globalThis.NSColor !== 'undefined') { + globalThis.UIColor = globalThis.NSColor; + cacheGlobal('UIColor', globalThis.UIColor); + } + var colorCtor = globalThis.UIColor || globalThis.NSColor; + if (colorCtor && colorCtor.prototype && + typeof colorCtor.prototype.initWithRedGreenBlueAlpha !== 'function') { + colorCtor.prototype.initWithRedGreenBlueAlpha = function(red, green, blue, alpha) { + if (typeof this.initWithSRGBRedGreenBlueAlpha === 'function') { + return this.initWithSRGBRedGreenBlueAlpha(red, green, blue, alpha); + } + if (typeof this.initWithCalibratedRedGreenBlueAlpha === 'function') { + return this.initWithCalibratedRedGreenBlueAlpha(red, green, blue, alpha); + } + if (typeof colorCtor.colorWithSRGBRedGreenBlueAlpha === 'function') { + return colorCtor.colorWithSRGBRedGreenBlueAlpha(red, green, blue, alpha); + } + if (typeof colorCtor.colorWithCalibratedRedGreenBlueAlpha === 'function') { + return colorCtor.colorWithCalibratedRedGreenBlueAlpha(red, green, blue, alpha); + } + return this; + }; + } + defineLazyGlobal('CC_SHA256', function() { return api.CC_SHA256; }); + + installInteropConstructors(); + installTypeScriptNativeHelpers(); + installInlineFunctions(); + + try { + Object.defineProperty(globalThis, installedFlagName, { + configurable: false, + enumerable: false, + writable: false, + value: true + }); + } catch (_) { + } +}) +)JSI_GLOBALS"; + + std::string script(GlobalInstaller); + script += "("; + script += jsStringLiteral(globalName); + script += ");"; + runtime.evaluateJavaScript(std::make_shared(std::move(script)), + "NativeApiJsiGlobals.js"); + NativeApiJsiWriteSmokeStage("jsi:globals:after-eval"); +} + +void InstallNativeApiJSI(Runtime& runtime, const NativeApiJsiConfig& config) { + const char* globalName = config.globalName != nullptr && config.globalName[0] != '\0' + ? config.globalName + : "__nativeScriptNativeApi"; + NativeApiJsiWriteSmokeStage("jsi:create-api"); + Object api = CreateNativeApiJSI(runtime, config); + Object global = runtime.global(); + NativeApiJsiWriteSmokeStage("jsi:set-global"); + global.setProperty(runtime, globalName, api); + + NativeApiJsiWriteSmokeStage("jsi:set-interop"); + Value existingInterop = global.getProperty(runtime, "interop"); + if (existingInterop.isUndefined() || existingInterop.isNull()) { + global.setProperty(runtime, "interop", api.getProperty(runtime, "interop")); + } + if (config.installGlobalSymbols) { + NativeApiJsiWriteSmokeStage("jsi:install-globals"); + InstallNativeApiJsiGlobalSymbols(runtime, globalName); + } else { + NativeApiJsiWriteSmokeStage("jsi:install-aggregate-globals"); + InstallAggregateGlobals(runtime, api, "protocolNames"); + } + NativeApiJsiWriteSmokeStage("jsi:installed"); +} diff --git a/NativeScript/ffi/shared/jsi/NativeApiJsiInvocation.h b/NativeScript/ffi/shared/jsi/NativeApiJsiInvocation.h new file mode 100644 index 00000000..d25c2145 --- /dev/null +++ b/NativeScript/ffi/shared/jsi/NativeApiJsiInvocation.h @@ -0,0 +1,510 @@ +bool isValidMetadataStringOffset(MDMetadataReader* metadata, + MDSectionOffset offset) { + if (metadata == nullptr || metadata->constantsOffset < metadata->stringsOffset) { + return false; + } + return offset < metadata->constantsOffset - metadata->stringsOffset; +} + +bool startsWith(const std::string& value, const std::string& prefix) { + return value.size() >= prefix.size() && + value.compare(0, prefix.size(), prefix) == 0; +} + +bool endsWith(const std::string& value, const std::string& suffix) { + return value.size() >= suffix.size() && + value.compare(value.size() - suffix.size(), suffix.size(), suffix) == 0; +} + +std::string stripEnumSuffix(const std::string& enumName) { + static const std::vector suffixes = { + "Options", "Option", "Enums", "Enum", "Result", "Direction", + "Orientation", "Style", "Mask", "Type", "Status", "Modes", "Mode", "s"}; + + for (const auto& suffix : suffixes) { + if (enumName.size() > suffix.size() && endsWith(enumName, suffix)) { + return enumName.substr(0, enumName.size() - suffix.size()); + } + } + + return enumName; +} + +bool isNSComparisonResultOrderingName(const std::string& enumName, + const std::string& member) { + if (enumName != "NSComparisonResult") { + return false; + } + return member == "Ascending" || member == "Same" || member == "Descending"; +} + +Value enumToObject(Runtime& runtime, MDMetadataReader* metadata, + const NativeApiSymbol& symbol) { + Object result(runtime); + if (metadata == nullptr || symbol.offset == MD_SECTION_OFFSET_NULL) { + return result; + } + + std::string enumName = symbol.name; + std::string strippedPrefix = stripEnumSuffix(enumName); + MDSectionOffset offset = symbol.offset + sizeof(MDSectionOffset); + bool next = true; + while (next) { + auto nameOffset = metadata->getOffset(offset); + next = (nameOffset & metagen::mdSectionOffsetNext) != 0; + nameOffset &= ~metagen::mdSectionOffsetNext; + offset += sizeof(MDSectionOffset); + + const char* memberName = metadata->resolveString(nameOffset); + int64_t value = metadata->getEnumValue(offset); + offset += sizeof(int64_t); + + std::string canonicalName = memberName != nullptr ? memberName : ""; + std::vector aliases; + aliases.push_back(canonicalName); + + if (!strippedPrefix.empty() && startsWith(canonicalName, strippedPrefix) && + canonicalName.size() > strippedPrefix.size()) { + aliases.push_back(canonicalName.substr(strippedPrefix.size())); + } else if (!strippedPrefix.empty() && + !startsWith(canonicalName, strippedPrefix)) { + aliases.push_back(strippedPrefix + canonicalName); + } + + if (startsWith(enumName, "NS") && !startsWith(canonicalName, "NS")) { + aliases.push_back(std::string("NS") + canonicalName); + } + + if (enumName == "NSStringCompareOptions" && + !endsWith(canonicalName, "Search")) { + aliases.push_back(canonicalName + "Search"); + aliases.push_back(std::string("NS") + canonicalName + "Search"); + } + + if (!startsWith(canonicalName, "k")) { + aliases.push_back(std::string("k") + enumName + canonicalName); + } + + if (isNSComparisonResultOrderingName(enumName, canonicalName)) { + aliases.push_back(std::string("Ordered") + canonicalName); + aliases.push_back(std::string("NSOrdered") + canonicalName); + } + + std::vector uniqueAliases; + std::unordered_set seenAliases; + for (const auto& alias : aliases) { + if (!alias.empty() && seenAliases.insert(alias).second) { + uniqueAliases.push_back(alias); + } + } + + for (const auto& alias : uniqueAliases) { + result.setProperty(runtime, alias.c_str(), static_cast(value)); + } + + char valueKey[32] = {}; + snprintf(valueKey, sizeof(valueKey), "%lld", static_cast(value)); + if (!result.hasProperty(runtime, valueKey)) { + std::string reverseName = + uniqueAliases.size() > 1 ? uniqueAliases[1] : canonicalName; + result.setProperty(runtime, valueKey, makeString(runtime, reverseName)); + } + } + return result; +} + +Value constantToValue(Runtime& runtime, + const std::shared_ptr& bridge, + const NativeApiSymbol& symbol) { + MDMetadataReader* metadata = bridge->metadata(); + if (metadata == nullptr || symbol.offset == MD_SECTION_OFFSET_NULL) { + return Value::undefined(); + } + + MDSectionOffset offset = symbol.offset + sizeof(MDSectionOffset); + auto evalKind = metadata->getVariableEvalKind(offset); + offset += sizeof(metagen::MDVariableEvalKind); + + switch (evalKind) { + case metagen::mdEvalInt64: + return static_cast(metadata->getInt64(offset)); + case metagen::mdEvalDouble: + return metadata->getDouble(offset); + case metagen::mdEvalString: { + if (isValidMetadataStringOffset(metadata, offset)) { + auto stringOffset = metadata->getOffset(offset); + return makeString(runtime, metadata->resolveString(stringOffset)); + } + + void* symbolPtr = dlsym(bridge->selfDl(), symbol.name.c_str()); + if (symbolPtr == nullptr) { + return Value::undefined(); + } + + NativeApiJsiType stringObjectType; + stringObjectType.kind = metagen::mdTypeNSStringObject; + stringObjectType.ffiType = &ffi_type_pointer; + stringObjectType.supported = true; + return convertNativeReturnValue(runtime, bridge, stringObjectType, + symbolPtr); + } + case metagen::mdEvalNone: + break; + } + + MDSectionOffset typeOffset = offset; + NativeApiJsiType type = parseMetadataJsiType(metadata, &typeOffset, bridge.get()); + if (unsupportedJsiType(type)) { + throw facebook::jsi::JSError( + runtime, "Native constant type is not supported by pure JSI: " + + symbol.name); + } + + void* symbolPtr = dlsym(bridge->selfDl(), symbol.name.c_str()); + if (symbolPtr == nullptr) { + return Value::undefined(); + } + return convertNativeReturnValue(runtime, bridge, type, symbolPtr); +} + +void prepareJsiArgument(Runtime& runtime, + const std::shared_ptr& bridge, + const NativeApiJsiType& type, const Value& arg, + size_t index, NativeApiJsiArgumentFrame& frame) { + ffi_type* ffiType = ffiTypeForJsiArgument(type); + size_t size = + ffiType != nullptr && ffiType->size > 0 ? ffiType->size : nativeSizeForType(type); + void* target = frame.storageAt(index, size); + convertJsiFfiArgument(runtime, bridge, type, arg, target, frame); +} + +void prepareJsiArguments(Runtime& runtime, + const std::shared_ptr& bridge, + const NativeApiJsiSignature& signature, + const Value* args, size_t count, + NativeApiJsiArgumentFrame& frame) { + if (count != signature.argumentTypes.size()) { + throw facebook::jsi::JSError( + runtime, "Actual arguments count: \"" + std::to_string(count) + + "\". Expected: \"" + + std::to_string(signature.argumentTypes.size()) + "\"."); + } + + for (size_t i = 0; i < signature.argumentTypes.size(); i++) { + prepareJsiArgument(runtime, bridge, signature.argumentTypes[i], args[i], i, + frame); + } +} + +Value callNativeFunctionPointer( + Runtime& runtime, const std::shared_ptr& bridge, + const NativeApiJsiType& type, void* pointer, bool block, const Value* args, + size_t count) { + if (pointer == nullptr) { + throw facebook::jsi::JSError(runtime, "Native function pointer is null."); + } + if (bridge == nullptr || bridge->metadata() == nullptr || + type.signatureOffset == MD_SECTION_OFFSET_NULL) { + throw facebook::jsi::JSError( + runtime, "Native function pointer metadata is unavailable."); + } + + auto signature = parseMetadataJsiSignature( + bridge->metadata(), type.signatureOffset, block ? 1 : 0, bridge.get()); + if (!signature || !signature->prepared || signature->variadic || + unsupportedJsiType(signature->returnType)) { + throw facebook::jsi::JSError( + runtime, + "Native function pointer signature is not supported by pure JSI."); + } + + NativeApiJsiArgumentFrame frame(signature->argumentTypes.size()); + prepareJsiArguments(runtime, bridge, *signature, args, count, frame); + + std::vector values; + if (block) { + values.reserve(signature->argumentTypes.size() + 1); + values.push_back(&pointer); + for (size_t i = 0; i < signature->argumentTypes.size(); i++) { + values.push_back(frame.values()[i]); + } + } + + void* callable = pointer; + if (block) { + auto literal = static_cast(pointer); + if (literal == nullptr || literal->invoke == nullptr) { + throw facebook::jsi::JSError(runtime, "Native block invoke pointer is null."); + } + callable = literal->invoke; + } + + std::vector returnStorage( + std::max(nativeSizeForType(signature->returnType), sizeof(void*)), 0); + performNativeInvocation(runtime, bridge->nativeInvocationInvoker(), [&]() { + ffi_call(&signature->cif, FFI_FN(callable), returnStorage.data(), + block ? values.data() : frame.values()); + }); + + return convertNativeReturnValue(runtime, bridge, signature->returnType, + returnStorage.data()); +} + +Value wrapNativeFunctionPointer(Runtime& runtime, + const std::shared_ptr& bridge, + const NativeApiJsiType& type, void* pointer, + bool block) { + const char* functionName = block ? "NativeApiJsiBlock" : "NativeApiJsiFunctionPointer"; + auto function = Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, functionName), 0, + [bridge, type, pointer, block](Runtime& runtime, const Value&, + const Value* args, size_t count) -> Value { + return callNativeFunctionPointer(runtime, bridge, type, pointer, block, + args, count); + }); + function.setProperty(runtime, "kind", + makeString(runtime, block ? "block" : "functionPointer")); + function.setProperty( + runtime, "__nativeApiPointerObject", + createPointer(runtime, bridge, pointer)); + function.setProperty( + runtime, "__nativeApiPointer", + static_cast(reinterpret_cast(pointer))); + function.setProperty( + runtime, "nativeAddress", + static_cast(reinterpret_cast(pointer))); + function.setProperty(runtime, "sizeof", + static_cast(sizeof(void*))); + function.setProperty( + runtime, "toString", + Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "toString"), 0, + [pointer, block](Runtime& runtime, const Value&, const Value*, + size_t) -> Value { + char address[32] = {}; + snprintf(address, sizeof(address), "%p", pointer); + return makeString(runtime, + std::string("[NativeApiJsi ") + + (block ? "Block " : "FunctionPointer ") + + address + "]"); + })); + return function; +} + +Value callCFunction(Runtime& runtime, + const std::shared_ptr& bridge, + const NativeApiSymbol& symbol, const Value* args, + size_t count) { + MDMetadataReader* metadata = bridge->metadata(); + if (metadata == nullptr) { + throw facebook::jsi::JSError(runtime, "Native metadata is not loaded."); + } + + void* fnptr = dlsym(bridge->selfDl(), symbol.name.c_str()); + if (fnptr == nullptr) { + throw facebook::jsi::JSError(runtime, + "Native function is not available: " + + symbol.name); + } + + MDSectionOffset signatureOffset = + metadata->signaturesOffset + + metadata->getOffset(symbol.offset + sizeof(MDSectionOffset)); + auto signature = parseMetadataJsiSignature( + metadata, signatureOffset, 0, bridge.get(), + (metadata->getFunctionFlag(symbol.offset + sizeof(MDSectionOffset) * 2) & + metagen::mdFunctionReturnOwned) != 0); + if (!signature || !signature->prepared || signature->variadic || + unsupportedJsiType(signature->returnType)) { + throw facebook::jsi::JSError( + runtime, "Native function signature is not supported by pure JSI: " + + symbol.name); + } + + NativeApiJsiArgumentFrame frame(signature->argumentTypes.size()); + prepareJsiArguments(runtime, bridge, *signature, args, count, frame); + + if (symbol.name == "NSApplicationMain" || + symbol.name == "UIApplicationMain") { + runtime.drainMicrotasks(); + } + + std::vector returnStorage( + std::max(nativeSizeForType(signature->returnType), sizeof(void*)), 0); + bool dispatchingNativeCallToUI = shouldDispatchNativeCallToUI(); + bool retainedReturn = false; + performNativeInvocation(runtime, bridge->nativeInvocationInvoker(), [&]() { + ffi_call(&signature->cif, FFI_FN(fnptr), returnStorage.data(), + frame.values()); + if (dispatchingNativeCallToUI && + !signature->returnType.returnOwned && + isObjectiveCObjectType(signature->returnType)) { + id object = *reinterpret_cast(returnStorage.data()); + if (object != nil) { + [object retain]; + retainedReturn = true; + } + } + }); + + NativeApiJsiType returnType = signature->returnType; + if (retainedReturn) { + returnType.returnOwned = true; + } + if (symbol.name == "CFBagContainsValue" && + (returnType.kind == metagen::mdTypeChar || + returnType.kind == metagen::mdTypeUChar || + returnType.kind == metagen::mdTypeUInt8)) { + return *returnStorage.data() != 0; + } + return convertNativeReturnValue(runtime, bridge, returnType, + returnStorage.data()); +} + +bool signatureSupportedForJsiInvocation( + const std::optional& signature) { + if (!signature || !signature->prepared || signature->variadic || + unsupportedJsiType(signature->returnType)) { + return false; + } + for (const auto& argType : signature->argumentTypes) { + if (unsupportedJsiType(argType)) { + return false; + } + } + return true; +} + +Value callObjCSelector(Runtime& runtime, + const std::shared_ptr& bridge, + id receiver, bool receiverIsClass, + const std::string& selectorName, + const NativeApiMember* member, + const Value* args, size_t count, + Class dispatchSuperClass) { + if (receiver == nil) { + throw facebook::jsi::JSError(runtime, + "Cannot send Objective-C selector to nil."); + } + + SEL selector = sel_registerName(selectorName.c_str()); + Class receiverClass = + receiverIsClass ? static_cast(receiver) : object_getClass(receiver); + Class lookupClass = dispatchSuperClass != Nil ? dispatchSuperClass : receiverClass; + Method method = receiverIsClass ? class_getClassMethod(lookupClass, selector) + : class_getInstanceMethod(lookupClass, selector); + if (method == nullptr && + (dispatchSuperClass != Nil || ![receiver respondsToSelector:selector])) { + throw facebook::jsi::JSError(runtime, + "Objective-C selector is not available: " + + selectorName); + } + + std::optional signature; + if (member != nullptr && + member->signatureOffset != MD_SECTION_OFFSET_NULL && + member->signatureOffset != 0) { + signature = parseMetadataJsiSignature( + bridge->metadata(), member->signatureOffset, 2, bridge.get(), + (member->flags & metagen::mdMemberReturnOwned) != 0); + } + if (!signatureSupportedForJsiInvocation(signature) && method != nullptr) { + signature = parseObjCMethodJsiSignature(method, bridge.get()); + } + + if (!signatureSupportedForJsiInvocation(signature)) { + throw facebook::jsi::JSError( + runtime, "Objective-C signature is not supported by pure JSI: " + + selectorName); + } + signature->selectorName = selectorName; + + NativeApiJsiArgumentFrame frame(signature->argumentTypes.size()); + const bool isNSErrorOutMethod = isNSErrorOutJsiMethodSignature(*signature); + if (isNSErrorOutMethod) { + size_t expected = signature->argumentTypes.size(); + if (count > expected || count + 1 < expected) { + throw facebook::jsi::JSError( + runtime, "Actual arguments count: \"" + std::to_string(count) + + "\". Expected: \"" + std::to_string(expected) + "\"."); + } + } + + const bool hasImplicitNSErrorOutArg = + isNSErrorOutMethod && count + 1 == signature->argumentTypes.size(); + NSError* implicitNSError = nil; + if (hasImplicitNSErrorOutArg) { + for (size_t i = 0; i < count; i++) { + prepareJsiArgument(runtime, bridge, signature->argumentTypes[i], args[i], i, + frame); + } + + size_t outArgIndex = signature->argumentTypes.size() - 1; + void* target = frame.storageAt(outArgIndex, sizeof(NSError**)); + NSError** implicitNSErrorOutArg = &implicitNSError; + *static_cast(target) = implicitNSErrorOutArg; + } else { + prepareJsiArguments(runtime, bridge, *signature, args, count, frame); + } + + std::vector values; + values.reserve(signature->argumentTypes.size() + 2); + struct objc_super superReceiver = {receiver, dispatchSuperClass}; + struct objc_super* superReceiverPtr = &superReceiver; + if (dispatchSuperClass != Nil) { + values.push_back(&superReceiverPtr); + } else { + values.push_back(&receiver); + } + values.push_back(&selector); + for (size_t i = 0; i < signature->argumentTypes.size(); i++) { + values.push_back(frame.values()[i]); + } + + std::vector returnStorage( + std::max(nativeSizeForType(signature->returnType), sizeof(void*)), 0); + bool dispatchingNativeCallToUI = shouldDispatchNativeCallToUI(); + bool retainedReturn = false; + performNativeInvocation(runtime, bridge->nativeInvocationInvoker(), [&]() { +#if defined(__x86_64__) + bool isStret = signature->returnType.ffiType->size > 16 && + signature->returnType.ffiType->type == FFI_TYPE_STRUCT; + void* target = dispatchSuperClass != Nil + ? (isStret ? FFI_FN(objc_msgSendSuper_stret) + : FFI_FN(objc_msgSendSuper)) + : (isStret ? FFI_FN(objc_msgSend_stret) + : FFI_FN(objc_msgSend)); + ffi_call(&signature->cif, target, returnStorage.data(), values.data()); +#else + ffi_call(&signature->cif, + dispatchSuperClass != Nil ? FFI_FN(objc_msgSendSuper) + : FFI_FN(objc_msgSend), + returnStorage.data(), values.data()); +#endif + if (dispatchingNativeCallToUI && + !signature->returnType.returnOwned && + isObjectiveCObjectType(signature->returnType)) { + id object = *reinterpret_cast(returnStorage.data()); + if (object != nil) { + [object retain]; + retainedReturn = true; + } + } + }); + + NativeApiJsiType returnType = signature->returnType; + if ((selectorName == "valueForKey:" || selectorName == "valueForKeyPath:") && + isObjectiveCObjectType(returnType)) { + returnType.kind = metagen::mdTypeAnyObject; + } + if (retainedReturn) { + returnType.returnOwned = true; + } + if (hasImplicitNSErrorOutArg && implicitNSError != nil) { + const char* errorMessage = [[implicitNSError description] UTF8String]; + throw facebook::jsi::JSError( + runtime, errorMessage != nullptr ? errorMessage : "Unknown NSError"); + } + return convertNativeReturnValue(runtime, bridge, returnType, + returnStorage.data()); +} diff --git a/NativeScript/ffi/v8/NativeApiV8.h b/NativeScript/ffi/v8/NativeApiV8.h new file mode 100644 index 00000000..cf8c4054 --- /dev/null +++ b/NativeScript/ffi/v8/NativeApiV8.h @@ -0,0 +1,21 @@ +#ifndef NATIVESCRIPT_FFI_V8_NATIVE_API_V8_H +#define NATIVESCRIPT_FFI_V8_NATIVE_API_V8_H + +#include "ffi/shared/direct/NativeApiDirect.h" +#include "v8.h" + +namespace nativescript { + +using NativeApiV8Config = NativeApiDirectConfig; + +void InstallNativeApiV8(v8::Isolate* isolate, + v8::Local context, + const NativeApiV8Config& config = NativeApiV8Config{}); + +} // namespace nativescript + +extern "C" void NativeScriptInstallNativeApiV8(v8::Isolate* isolate, + v8::Local context, + const char* metadataPath); + +#endif // NATIVESCRIPT_FFI_V8_NATIVE_API_V8_H diff --git a/NativeScript/ffi/v8/NativeApiV8.mm b/NativeScript/ffi/v8/NativeApiV8.mm new file mode 100644 index 00000000..1a22acca --- /dev/null +++ b/NativeScript/ffi/v8/NativeApiV8.mm @@ -0,0 +1,179 @@ +#include "NativeApiV8.h" + +#ifdef TARGET_ENGINE_V8 + +#include "NativeApiV8Runtime.h" + +namespace nativescript { + +using NativeApiJsiConfig = NativeApiDirectConfig; +using NativeApiJsiScheduler = NativeApiDirectScheduler; + +namespace { + +using facebook::jsi::Array; +using facebook::jsi::ArrayBuffer; +using facebook::jsi::BigInt; +using facebook::jsi::Function; +using facebook::jsi::HostObject; +using facebook::jsi::MutableBuffer; +using facebook::jsi::Object; +using facebook::jsi::PropNameID; +using facebook::jsi::Runtime; +using facebook::jsi::String; +using facebook::jsi::StringBuffer; +using facebook::jsi::Value; +using metagen::MDMemberFlag; +using metagen::MDMetadataReader; +using metagen::MDSectionOffset; +using metagen::MDTypeKind; + +// clang-format off +#include "jsi/NativeApiJsiBridge.h" +// clang-format on + +#define NATIVESCRIPT_NATIVE_API_HAS_ENGINE_LAZY_GLOBALS 1 +#define NATIVESCRIPT_NATIVE_API_RETAIN_RUNTIME 1 +#define NATIVESCRIPT_NATIVE_API_RUNTIME_SCOPE 1 + +struct NativeApiV8LazyGlobalData { + NativeApiV8LazyGlobalData(v8::Isolate* isolate, const std::string& name, + const std::string& kind) { + nameValue.Reset(isolate, facebook::jsi::v8direct::makeV8String(isolate, name)); + kindValue.Reset(isolate, facebook::jsi::v8direct::makeV8String(isolate, kind)); + } + + ~NativeApiV8LazyGlobalData() { + nameValue.Reset(); + kindValue.Reset(); + } + + v8::Global nameValue; + v8::Global kindValue; +}; + +std::shared_ptr retainNativeApiJsiRuntime(Runtime& runtime) { + return std::make_shared(runtime.state()); +} + +class NativeApiJsiRuntimeScope final { + public: + explicit NativeApiJsiRuntimeScope(Runtime& runtime) + : locker_(runtime.isolate()), + isolateScope_(runtime.isolate()), + handleScope_(runtime.isolate()), + context_(runtime.context()), + contextScope_(context_) {} + + private: + v8::Locker locker_; + v8::Isolate::Scope isolateScope_; + v8::HandleScope handleScope_; + v8::Local context_; + v8::Context::Scope contextScope_; +}; + +void NativeApiV8LazyGlobalGetter(v8::Local, + const v8::PropertyCallbackInfo& info) { + v8::Isolate* isolate = info.GetIsolate(); + v8::HandleScope handleScope(isolate); + v8::Local context = isolate->GetCurrentContext(); + if (!info.Data()->IsExternal()) { + return; + } + + auto* data = static_cast(info.Data().As()->Value()); + if (data == nullptr) { + return; + } + v8::Local nameValue = data->nameValue.Get(isolate); + v8::Local kindValue = data->kindValue.Get(isolate); + + v8::Local global = context->Global(); + v8::Local resolverValue; + if (!global + ->Get(context, facebook::jsi::v8direct::makeV8String( + isolate, "__nativeScriptResolveNativeApiLazyGlobal")) + .ToLocal(&resolverValue) || + !resolverValue->IsFunction()) { + return; + } + + v8::TryCatch tryCatch(isolate); + v8::Local args[] = {nameValue, kindValue}; + v8::Local result; + if (!resolverValue.As()->Call(context, global, 2, args).ToLocal(&result)) { + if (tryCatch.HasCaught()) { + isolate->ThrowException(tryCatch.Exception()); + } + return; + } + global->DefineOwnProperty(context, nameValue, result, v8::DontEnum).FromMaybe(false); + info.GetReturnValue().Set(result); +} + +bool InstallNativeApiEngineLazyGlobal(Runtime& runtime, std::shared_ptr, + const std::string& name, const std::string& kind, + bool force) { + if (name.empty() || kind.empty()) { + return false; + } + + v8::Isolate* isolate = runtime.isolate(); + v8::EscapableHandleScope handleScope(isolate); + v8::Local context = runtime.context(); + v8::Local global = context->Global(); + v8::Local property = facebook::jsi::v8direct::makeV8String(isolate, name); + if (!force && global->HasOwnProperty(context, property).FromMaybe(false)) { + return false; + } + + auto data = std::make_shared(isolate, name, kind); + v8::Local external = v8::External::New(isolate, data.get()); + + bool installed = global + ->SetNativeDataProperty(context, property, NativeApiV8LazyGlobalGetter, + nullptr, external, v8::DontEnum) + .FromMaybe(false); + if (installed) { + runtime.state()->retainedNativeData.push_back(std::move(data)); + } + return installed; +} + +// clang-format off +#include "jsi/NativeApiJsiHostObjects.h" +#include "jsi/NativeApiJsiCallbacks.h" +#include "jsi/NativeApiJsiConversion.h" +#include "jsi/NativeApiJsiInvocation.h" +#include "jsi/NativeApiJsiClassBuilder.h" +#include "jsi/NativeApiJsiHostObject.h" +// clang-format on + +} // namespace + +#include "jsi/NativeApiJsiInstall.h" + +void InstallNativeApiV8(v8::Isolate* isolate, v8::Local context, + const NativeApiV8Config& config) { + if (isolate == nullptr || context.IsEmpty()) { + return; + } + v8::Locker locker(isolate); + v8::Isolate::Scope isolateScope(isolate); + v8::HandleScope handleScope(isolate); + v8::Context::Scope contextScope(context); + Runtime runtime(isolate, context); + InstallNativeApiJSI(runtime, config); +} + +} // namespace nativescript + +extern "C" void NativeScriptInstallNativeApiV8(v8::Isolate* isolate, v8::Local context, + const char* metadataPath) { + nativescript::NativeApiV8Config config; + config.metadataPath = metadataPath; + nativescript::InstallNativeApiV8(isolate, context, config); +} + +#endif // TARGET_ENGINE_V8 diff --git a/NativeScript/ffi/v8/NativeApiV8HostObjects.mm b/NativeScript/ffi/v8/NativeApiV8HostObjects.mm new file mode 100644 index 00000000..8a11b15c --- /dev/null +++ b/NativeScript/ffi/v8/NativeApiV8HostObjects.mm @@ -0,0 +1,187 @@ +#include "NativeApiV8Runtime.h" + +#ifdef TARGET_ENGINE_V8 + +namespace facebook { +namespace jsi { + +namespace v8direct { + +Value valueFromLocal(Runtime& runtime, v8::Local value) { return Value(runtime, value); } + +v8::Local hostObjectTemplate(Runtime& runtime) { + auto state = runtime.state(); + if (state->hostObjectTemplate.IsEmpty()) { + v8::Local objectTemplate = v8::ObjectTemplate::New(runtime.isolate()); + objectTemplate->SetInternalFieldCount(1); + objectTemplate->SetHandler(v8::NamedPropertyHandlerConfiguration( + [](v8::Local property, + const v8::PropertyCallbackInfo& info) -> v8::Intercepted { + auto* holder = + static_cast(info.Holder()->GetAlignedPointerFromInternalField(0)); + if (holder == nullptr || holder->hostObject == nullptr) { + return v8::Intercepted::kNo; + } + Runtime runtime(holder->state); + try { + Value result = holder->hostObject->get( + runtime, PropNameID(propertyNameToUtf8(info.GetIsolate(), property))); + if (!result.isUndefined()) { + info.GetReturnValue().Set(result.local(runtime)); + return v8::Intercepted::kYes; + } + } catch (const std::exception& exception) { + throwV8Exception(info.GetIsolate(), exception); + return v8::Intercepted::kYes; + } + return v8::Intercepted::kNo; + }, + [](v8::Local property, v8::Local value, + const v8::PropertyCallbackInfo& info) -> v8::Intercepted { + auto* holder = + static_cast(info.Holder()->GetAlignedPointerFromInternalField(0)); + if (holder == nullptr || holder->hostObject == nullptr) { + return v8::Intercepted::kNo; + } + Runtime runtime(holder->state); + try { + holder->hostObject->set(runtime, + PropNameID(propertyNameToUtf8(info.GetIsolate(), property)), + Value(runtime, value)); + return v8::Intercepted::kYes; + } catch (const std::exception& exception) { + throwV8Exception(info.GetIsolate(), exception); + return v8::Intercepted::kYes; + } + }, + nullptr, nullptr, + [](const v8::PropertyCallbackInfo& info) { + auto* holder = + static_cast(info.Holder()->GetAlignedPointerFromInternalField(0)); + if (holder == nullptr || holder->hostObject == nullptr) { + return; + } + Runtime runtime(holder->state); + try { + auto propertyNames = holder->hostObject->getPropertyNames(runtime); + v8::Local result = + v8::Array::New(info.GetIsolate(), static_cast(propertyNames.size())); + for (size_t i = 0; i < propertyNames.size(); i++) { + std::string name = propertyNames[i].utf8(runtime); + result + ->Set(runtime.context(), static_cast(i), + makeV8String(info.GetIsolate(), name)) + .FromMaybe(false); + } + info.GetReturnValue().Set(result); + } catch (const std::exception& exception) { + throwV8Exception(info.GetIsolate(), exception); + } + }, + v8::Local(), v8::PropertyHandlerFlags::kNone)); + objectTemplate->SetHandler(v8::IndexedPropertyHandlerConfiguration( + [](uint32_t index, const v8::PropertyCallbackInfo& info) -> v8::Intercepted { + auto* holder = + static_cast(info.Holder()->GetAlignedPointerFromInternalField(0)); + if (holder == nullptr || holder->hostObject == nullptr) { + return v8::Intercepted::kNo; + } + Runtime runtime(holder->state); + try { + Value result = holder->hostObject->get(runtime, PropNameID(std::to_string(index))); + if (!result.isUndefined()) { + info.GetReturnValue().Set(result.local(runtime)); + return v8::Intercepted::kYes; + } + } catch (const std::exception& exception) { + throwV8Exception(info.GetIsolate(), exception); + return v8::Intercepted::kYes; + } + return v8::Intercepted::kNo; + }, + [](uint32_t index, v8::Local value, + const v8::PropertyCallbackInfo& info) -> v8::Intercepted { + auto* holder = + static_cast(info.Holder()->GetAlignedPointerFromInternalField(0)); + if (holder == nullptr || holder->hostObject == nullptr) { + return v8::Intercepted::kNo; + } + Runtime runtime(holder->state); + try { + holder->hostObject->set(runtime, PropNameID(std::to_string(index)), + Value(runtime, value)); + return v8::Intercepted::kYes; + } catch (const std::exception& exception) { + throwV8Exception(info.GetIsolate(), exception); + return v8::Intercepted::kYes; + } + }, + nullptr, nullptr, nullptr, v8::Local(), v8::PropertyHandlerFlags::kNone)); + state->hostObjectTemplate.Reset(runtime.isolate(), objectTemplate); + } + return state->hostObjectTemplate.Get(runtime.isolate()); +} + +void hostObjectWeakCallback(const v8::WeakCallbackInfo& info) { + delete info.GetParameter(); +} + +void functionWeakCallback(const v8::WeakCallbackInfo& info) { + delete info.GetParameter(); +} + +} // namespace v8direct + +Object Object::createFromHostObjectWithToken(Runtime& runtime, std::shared_ptr host, + const void* typeToken) { + v8::Local object = + v8direct::hostObjectTemplate(runtime)->NewInstance(runtime.context()).ToLocalChecked(); + auto* holder = new v8direct::HostObjectHolder(runtime.state(), std::move(host), typeToken); + object->SetAlignedPointerInInternalField(0, holder); + holder->object.Reset(runtime.isolate(), object); + holder->object.SetWeak(holder, v8direct::hostObjectWeakCallback, + v8::WeakCallbackType::kParameter); + return Object::fromValueStorage(Value(runtime, object).storage_); +} + +Function Function::createFromHostFunction(Runtime& runtime, const PropNameID& name, unsigned int, + HostFunctionType callback) { + auto* holder = new v8direct::FunctionHolder(runtime.state(), std::move(callback)); + v8::Local data = v8::External::New(runtime.isolate(), holder); + v8::Local functionTemplate = v8::FunctionTemplate::New( + runtime.isolate(), + [](const v8::FunctionCallbackInfo& info) { + auto* holder = + static_cast(info.Data().As()->Value()); + Runtime runtime(holder->state); + std::vector args; + args.reserve(info.Length()); + for (int i = 0; i < info.Length(); i++) { + args.push_back(Value(runtime, info[i])); + } + try { + Value thisValue(runtime, info.This()); + Value result = holder->callback(runtime, thisValue, args.empty() ? nullptr : args.data(), + args.size()); + info.GetReturnValue().Set(result.local(runtime)); + } catch (const std::exception& exception) { + v8direct::throwV8Exception(info.GetIsolate(), exception); + } + }, + data); + v8::Local function = + functionTemplate->GetFunction(runtime.context()).ToLocalChecked(); + std::string functionName = name.utf8(runtime); + if (!functionName.empty()) { + function->SetName(v8direct::makeV8String(runtime.isolate(), functionName)); + } + holder->function.Reset(runtime.isolate(), function); + holder->function.SetWeak(holder, v8direct::functionWeakCallback, + v8::WeakCallbackType::kParameter); + return Function(Object::fromValueStorage(Value(runtime, function).storage_)); +} + +} // namespace jsi +} // namespace facebook + +#endif // TARGET_ENGINE_V8 diff --git a/NativeScript/ffi/v8/NativeApiV8Runtime.h b/NativeScript/ffi/v8/NativeApiV8Runtime.h new file mode 100644 index 00000000..81c95a08 --- /dev/null +++ b/NativeScript/ffi/v8/NativeApiV8Runtime.h @@ -0,0 +1,736 @@ +#ifndef NATIVESCRIPT_FFI_V8_NATIVE_API_V8_RUNTIME_H +#define NATIVESCRIPT_FFI_V8_NATIVE_API_V8_RUNTIME_H + +#ifdef TARGET_ENGINE_V8 + +#import +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Metadata.h" +#include "MetadataReader.h" +#include "ffi.h" +#include "v8.h" + +@protocol NativeApiJsiClassBuilderProtocol +@end + +#ifdef EMBED_METADATA_SIZE +extern const unsigned char embedded_metadata[EMBED_METADATA_SIZE]; +#endif + +namespace facebook { +namespace jsi { + +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 void set(Runtime& runtime, const PropNameID& name, const Value& value); + virtual std::vector getPropertyNames(Runtime& runtime); +}; + +using HostFunctionType = std::function; + +namespace v8direct { + +struct RuntimeState { + explicit RuntimeState(v8::Isolate* isolate, v8::Local context) : isolate(isolate) { + this->context.Reset(isolate, context); + } + + ~RuntimeState() { context.Reset(); } + + v8::Local localContext() const { return context.Get(isolate); } + + v8::Isolate* isolate = nullptr; + v8::Global context; + v8::Global hostObjectTemplate; + std::vector> retainedNativeData; +}; + +struct ValueStorage { + enum class Kind { + Undefined, + Null, + Bool, + Number, + V8, + }; + + explicit ValueStorage(Kind kind) : kind(kind) {} + + ~ValueStorage() { value.Reset(); } + + Kind kind = Kind::Undefined; + bool boolValue = false; + double numberValue = 0; + v8::Global value; +}; + +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; +}; + +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 direct V8 operation failed."; +} + +inline void throwV8Exception(v8::Isolate* isolate, const std::exception& exception) { + isolate->ThrowException(v8::Exception::Error(makeV8String(isolate, exception.what()))); +} + +} // namespace v8direct + +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(); } + std::shared_ptr state() const { return state_; } + + 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, + v8direct::makeV8String(runtime.isolate(), value != nullptr ? value : "")); + } + + static String createFromUtf8(Runtime& runtime, const std::string& value) { + return String(runtime, v8direct::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 v8direct::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() + : storage_( + std::make_shared(v8direct::ValueStorage::Kind::Undefined)) {} + + Value(bool value) + : storage_(std::make_shared(v8direct::ValueStorage::Kind::Bool)) { + storage_->boolValue = value; + } + + Value(double value) + : storage_(std::make_shared(v8direct::ValueStorage::Kind::Number)) { + storage_->numberValue = value; + } + + Value(int value) : Value(static_cast(value)) {} + Value(uint32_t value) : Value(static_cast(value)) {} + + Value(Runtime& runtime, const Value& value) : storage_(value.storage_) {} + Value(Runtime& runtime, Value&& value) : storage_(std::move(value.storage_)) {} + Value(Runtime& runtime, const String& value) : storage_(value.storage_) {} + 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.storage_ = std::make_shared(v8direct::ValueStorage::Kind::Null); + return value; + } + + 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 (storage_->kind) { + case v8direct::ValueStorage::Kind::Undefined: + return v8::Undefined(isolate); + case v8direct::ValueStorage::Kind::Null: + return v8::Null(isolate); + case v8direct::ValueStorage::Kind::Bool: + return v8::Boolean::New(isolate, storage_->boolValue); + case v8direct::ValueStorage::Kind::Number: + return v8::Number::New(isolate, storage_->numberValue); + case v8direct::ValueStorage::Kind::V8: + return storage_->value.Get(isolate); + } + } + + Value(Runtime& runtime, v8::Local value) + : storage_(std::make_shared(v8direct::ValueStorage::Kind::V8)) { + storage_->value.Reset(runtime.isolate(), value); + } + + private: + friend class Runtime; + friend class Object; + friend class String; + friend class BigInt; + friend class ArrayBuffer; + friend class Function; + friend class Array; + + std::shared_ptr storage_; +}; + +class Object { + public: + Object() = default; + explicit Object(Runtime& runtime) + : storage_(std::make_shared(v8direct::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), + v8direct::hostObjectTypeToken()); + } + + Value getProperty(Runtime& runtime, const char* name) const { + return getProperty(runtime, + v8direct::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, v8direct::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, v8direct::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, v8direct::currentExceptionMessage(runtime.isolate(), tryCatch)); + } + } + + bool hasProperty(Runtime& runtime, const char* name) const { + v8::TryCatch tryCatch(runtime.isolate()); + return local(runtime) + ->Has(runtime.context(), + v8direct::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 == v8direct::hostObjectTypeToken(); + } + + template + std::shared_ptr getHostObject(Runtime& runtime) const { + auto holder = hostObjectHolder(runtime); + if (holder == nullptr || holder->typeToken != v8direct::hostObjectTypeToken()) { + return nullptr; + } + return std::static_pointer_cast(holder->hostObject); + } + + v8::Local local(Runtime& runtime) const { + return storage_->value.Get(runtime.isolate()).As(); + } + + operator Value() const { + Value value; + value.storage_ = storage_; + return value; + } + + 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); + + v8direct::HostObjectHolder* hostObjectHolder(Runtime& runtime) const { + v8::Local object = local(runtime); + if (object->InternalFieldCount() < 1) { + return nullptr; + } + return static_cast(object->GetAlignedPointerFromInternalField(0)); + } + + 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, v8direct::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, v8direct::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, v8direct::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 { + Value value; + value.storage_ = storage_; + return value; + } +}; + +class Array : public Object { + public: + explicit Array(Runtime& runtime, size_t size) + : Object(std::make_shared(v8direct::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, v8direct::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, v8direct::currentExceptionMessage(runtime.isolate(), tryCatch)); + } + } + + void setValueAtIndex(Runtime& runtime, size_t index, const String& value) { + setValueAtIndex(runtime, index, Value(runtime, value)); + } + + operator Value() const { + Value value; + value.storage_ = storage_; + return value; + } +}; + +class BigInt { + public: + BigInt() = default; + BigInt(Runtime& runtime, v8::Local value) + : storage_(std::make_shared(v8direct::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, v8direct::currentExceptionMessage(runtime.isolate(), tryCatch)); + } + return String(runtime, result); + } + + v8::Local local(Runtime& runtime) const { + return storage_->value.Get(runtime.isolate()).As(); + } + + operator Value() const { + Value value; + value.storage_ = storage_; + return value; + } + + private: + friend class Value; + std::shared_ptr storage_; +}; + +class ArrayBuffer : public Object { + public: + ArrayBuffer(Runtime& runtime, std::shared_ptr buffer) + : Object(std::make_shared(v8direct::ValueStorage::Kind::V8)) { + auto holder = new v8direct::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 { + Value value; + value.storage_ = storage_; + return value; + } +}; +} // namespace jsi +} // namespace facebook + +#endif // TARGET_ENGINE_V8 + +#endif // NATIVESCRIPT_FFI_V8_NATIVE_API_V8_RUNTIME_H diff --git a/NativeScript/ffi/v8/NativeApiV8Runtime.mm b/NativeScript/ffi/v8/NativeApiV8Runtime.mm new file mode 100644 index 00000000..681a211b --- /dev/null +++ b/NativeScript/ffi/v8/NativeApiV8Runtime.mm @@ -0,0 +1,36 @@ +#include "NativeApiV8Runtime.h" + +#ifdef TARGET_ENGINE_V8 + +namespace facebook { +namespace jsi { + +Object Runtime::global() { + return Object::fromValueStorage(Value(*this, context()->Global()).storage_); +} + +Value Runtime::evaluateJavaScript(std::shared_ptr buffer, + const std::string& sourceURL) { + v8::TryCatch tryCatch(isolate()); + v8::Local source = + v8::String::NewFromUtf8(isolate(), buffer != nullptr ? buffer->data() : "", + v8::NewStringType::kNormal, + buffer != nullptr ? static_cast(buffer->size()) : 0) + .ToLocalChecked(); + v8::Local resourceName = v8direct::makeV8String(isolate(), sourceURL); + v8::ScriptOrigin origin(resourceName); + v8::Local script; + if (!v8::Script::Compile(context(), source, &origin).ToLocal(&script)) { + throw JSError(*this, v8direct::currentExceptionMessage(isolate(), tryCatch)); + } + v8::Local result; + if (!script->Run(context()).ToLocal(&result)) { + throw JSError(*this, v8direct::currentExceptionMessage(isolate(), tryCatch)); + } + return Value(*this, result); +} + +} // namespace jsi +} // namespace facebook + +#endif // TARGET_ENGINE_V8 diff --git a/NativeScript/ffi/v8/NativeApiV8Value.mm b/NativeScript/ffi/v8/NativeApiV8Value.mm new file mode 100644 index 00000000..d8b34428 --- /dev/null +++ b/NativeScript/ffi/v8/NativeApiV8Value.mm @@ -0,0 +1,177 @@ +#include "NativeApiV8Runtime.h" + +#ifdef TARGET_ENGINE_V8 + +namespace facebook { +namespace jsi { + +Value HostObject::get(Runtime&, const PropNameID&) { return Value::undefined(); } + +void HostObject::set(Runtime&, const PropNameID&, const Value&) {} + +std::vector HostObject::getPropertyNames(Runtime&) { return {}; } + +String::String(Runtime& runtime, v8::Local value) + : storage_(std::make_shared(v8direct::ValueStorage::Kind::V8)) { + storage_->value.Reset(runtime.isolate(), value); +} + +String::operator Value() const { + Value value; + value.storage_ = storage_; + return value; +} + +Value::Value(Runtime&, const Object& object) : storage_(object.storage_) {} +Value::Value(Runtime&, const Function& function) : storage_(function.storage_) {} +Value::Value(Runtime&, const Array& array) : storage_(array.storage_) {} +Value::Value(Runtime&, const ArrayBuffer& arrayBuffer) : storage_(arrayBuffer.storage_) {} +Value::Value(Runtime&, const BigInt& bigint) : storage_(bigint.storage_) {} + +bool Value::isObject() const { + if (storage_->kind != v8direct::ValueStorage::Kind::V8 || storage_->value.IsEmpty()) { + return false; + } + v8::Isolate* isolate = v8::Isolate::GetCurrent(); + return isolate != nullptr && storage_->value.Get(isolate)->IsObject(); +} + +bool Value::isUndefined() const { + if (storage_->kind == v8direct::ValueStorage::Kind::Undefined) { + return true; + } + if (storage_->kind != v8direct::ValueStorage::Kind::V8 || storage_->value.IsEmpty()) { + return false; + } + v8::Isolate* isolate = v8::Isolate::GetCurrent(); + return isolate != nullptr && storage_->value.Get(isolate)->IsUndefined(); +} + +bool Value::isNull() const { + if (storage_->kind == v8direct::ValueStorage::Kind::Null) { + return true; + } + if (storage_->kind != v8direct::ValueStorage::Kind::V8 || storage_->value.IsEmpty()) { + return false; + } + v8::Isolate* isolate = v8::Isolate::GetCurrent(); + return isolate != nullptr && storage_->value.Get(isolate)->IsNull(); +} + +bool Value::isBool() const { + if (storage_->kind == v8direct::ValueStorage::Kind::Bool) { + return true; + } + if (storage_->kind != v8direct::ValueStorage::Kind::V8 || storage_->value.IsEmpty()) { + return false; + } + v8::Isolate* isolate = v8::Isolate::GetCurrent(); + return isolate != nullptr && storage_->value.Get(isolate)->IsBoolean(); +} + +bool Value::getBool() const { + if (storage_->kind == v8direct::ValueStorage::Kind::Bool) { + return storage_->boolValue; + } + if (storage_->kind == v8direct::ValueStorage::Kind::V8 && !storage_->value.IsEmpty()) { + v8::Isolate* isolate = v8::Isolate::GetCurrent(); + if (isolate != nullptr) { + return storage_->value.Get(isolate)->BooleanValue(isolate); + } + } + return false; +} + +bool Value::isNumber() const { + if (storage_->kind == v8direct::ValueStorage::Kind::Number) { + return true; + } + if (storage_->kind != v8direct::ValueStorage::Kind::V8 || storage_->value.IsEmpty()) { + return false; + } + v8::Isolate* isolate = v8::Isolate::GetCurrent(); + return isolate != nullptr && storage_->value.Get(isolate)->IsNumber(); +} + +double Value::getNumber() const { + if (storage_->kind == v8direct::ValueStorage::Kind::Number) { + return storage_->numberValue; + } + if (storage_->kind == v8direct::ValueStorage::Kind::V8 && !storage_->value.IsEmpty()) { + v8::Isolate* isolate = v8::Isolate::GetCurrent(); + if (isolate != nullptr) { + return storage_->value.Get(isolate)->NumberValue(isolate->GetCurrentContext()).FromMaybe(0); + } + } + return 0; +} + +bool Value::isString() const { + if (storage_->kind != v8direct::ValueStorage::Kind::V8 || storage_->value.IsEmpty()) { + return false; + } + v8::Isolate* isolate = v8::Isolate::GetCurrent(); + return storage_->value.Get(isolate)->IsString(); +} + +bool Value::isBigInt() const { + if (storage_->kind != v8direct::ValueStorage::Kind::V8 || storage_->value.IsEmpty()) { + return false; + } + v8::Isolate* isolate = v8::Isolate::GetCurrent(); + return storage_->value.Get(isolate)->IsBigInt(); +} + +bool Value::isSymbol() const { + if (storage_->kind != v8direct::ValueStorage::Kind::V8 || storage_->value.IsEmpty()) { + return false; + } + v8::Isolate* isolate = v8::Isolate::GetCurrent(); + return storage_->value.Get(isolate)->IsSymbol(); +} + +Object Value::asObject(Runtime& runtime) const { return Object::fromValueStorage(storage_); } + +String Value::asString(Runtime& runtime) const { + return String(runtime, local(runtime).As()); +} + +BigInt Value::getBigInt(Runtime& runtime) const { + return BigInt(runtime, local(runtime).As()); +} + +Function Object::getPropertyAsFunction(Runtime& runtime, const char* name) const { + return getProperty(runtime, name).asObject(runtime).asFunction(runtime); +} + +Function Object::asFunction(Runtime& runtime) const { return Function(*this); } + +Array Object::getArray(Runtime& runtime) const { return Array(*this); } + +ArrayBuffer Object::getArrayBuffer(Runtime& runtime) const { return ArrayBuffer(*this); } + +Array Object::getPropertyNames(Runtime& runtime) const { + v8::TryCatch tryCatch(runtime.isolate()); + v8::Local result; + if (!local(runtime)->GetPropertyNames(runtime.context()).ToLocal(&result)) { + throw JSError(runtime, v8direct::currentExceptionMessage(runtime.isolate(), tryCatch)); + } + return Array(Object::fromValueStorage(Value(runtime, result).storage_)); +} + +void Object::setProperty(Runtime& runtime, const char* name, const Function& value) { + setProperty(runtime, name, Value(runtime, value)); +} + +void Object::setProperty(Runtime& runtime, const char* name, const Array& value) { + setProperty(runtime, name, Value(runtime, value)); +} + +void Object::setProperty(Runtime& runtime, const char* name, const ArrayBuffer& value) { + setProperty(runtime, name, Value(runtime, value)); +} + +} // namespace jsi +} // namespace facebook + +#endif // TARGET_ENGINE_V8 diff --git a/NativeScript/napi/hermes/jsr.cpp b/NativeScript/napi/hermes/jsr.cpp index 9bd91383..ad23e9b0 100644 --- a/NativeScript/napi/hermes/jsr.cpp +++ b/NativeScript/napi/hermes/jsr.cpp @@ -19,6 +19,14 @@ class RuntimeLockGuard { }; } // namespace +int js_current_env_lock_depth(napi_env env) { + auto itFound = JSR::env_to_jsr_cache.find(env); + if (itFound == JSR::env_to_jsr_cache.end() || itFound->second == nullptr) { + return 0; + } + return itFound->second->currentLockDepth(); +} + JSR::JSR() { hermes::vm::RuntimeConfig config = hermes::vm::RuntimeConfig::Builder() .withMicrotaskQueue(true) @@ -64,6 +72,14 @@ napi_status js_create_napi_env(napi_env* env, napi_runtime runtime) { return napi_ok; } +facebook::jsi::Runtime* js_get_jsi_runtime(napi_env env) { + auto itFound = JSR::env_to_jsr_cache.find(env); + if (itFound == JSR::env_to_jsr_cache.end()) { + return nullptr; + } + return itFound->second->rt; +} + napi_status js_set_runtime_flags(const char* flags) { return napi_ok; } napi_status js_free_napi_env(napi_env env) { @@ -138,6 +154,7 @@ extern "C" napi_status jsr_drain_microtasks(napi_env env, return napi_invalid_arg; } - *result = itFound->second->runtime->drainMicrotasks(max_count_hint); + NapiScope scope(env, false); + *result = itFound->second->rt->drainMicrotasks(max_count_hint); return napi_ok; } diff --git a/NativeScript/napi/hermes/jsr.h b/NativeScript/napi/hermes/jsr.h index 18154170..cb6221ab 100644 --- a/NativeScript/napi/hermes/jsr.h +++ b/NativeScript/napi/hermes/jsr.h @@ -9,24 +9,45 @@ #include "jsi/threadsafe.h" #include "jsr_common.h" +#include + class JSR { public: JSR(); std::unique_ptr runtime; facebook::jsi::Runtime* rt; std::recursive_mutex js_mutex; + static inline thread_local std::unordered_map lock_depth; void lock() { runtime->lock(); js_mutex.lock(); + lock_depth[this] += 1; } void unlock() { - runtime->unlock(); + auto depth = lock_depth.find(this); + if (depth != lock_depth.end()) { + depth->second -= 1; + if (depth->second <= 0) { + lock_depth.erase(depth); + } + } js_mutex.unlock(); + runtime->unlock(); + } + int currentLockDepth() const { + auto depth = lock_depth.find(const_cast(this)); + if (depth == lock_depth.end()) { + return 0; + } + return depth->second; } static std::unordered_map env_to_jsr_cache; }; +int js_current_env_lock_depth(napi_env env); +facebook::jsi::Runtime* js_get_jsi_runtime(napi_env env); + typedef struct napi_runtime__ { JSR* hermes; } napi_runtime__; diff --git a/NativeScript/napi/jsc/jsc-api.cpp b/NativeScript/napi/jsc/jsc-api.cpp index 50fcbf14..698b1061 100644 --- a/NativeScript/napi/jsc/jsc-api.cpp +++ b/NativeScript/napi/jsc/jsc-api.cpp @@ -596,8 +596,37 @@ class FunctionInfo : public NativeInfo { if (info == nullptr) { return napi_set_last_error(env, napi_generic_failure); } - JSObjectRef function = - JSObjectMake(env->context, xyz::functionInfoClass, info); + + JSString name(utf8name != nullptr ? utf8name : "", + utf8name != nullptr ? length : 0); + JSObjectRef function = JSObjectMakeFunctionWithCallback( + env->context, utf8name != nullptr ? static_cast(name) + : nullptr, + FunctionInfo::CallAsFunction); + if (function == nullptr) { + delete info; + return napi_set_last_error(env, napi_generic_failure); + } + + NativeInfo::SetNativeInfoKey(env->context, function, xyz::functionInfoClass, + env->function_info_symbol, info); + + JSValueRef exception{}; + JSObjectSetProperty(env->context, function, JSString("length"), + JSValueMakeNumber(env->context, 0), + kJSPropertyAttributeDontEnum | + kJSPropertyAttributeReadOnly | + kJSPropertyAttributeDontDelete, + &exception); + if (utf8name != nullptr) { + JSObjectSetProperty(env->context, function, JSString("name"), + JSValueMakeString(env->context, name), + kJSPropertyAttributeDontEnum | + kJSPropertyAttributeReadOnly | + kJSPropertyAttributeDontDelete, + &exception); + } + *result = ToNapi(function); return napi_ok; } @@ -638,6 +667,16 @@ class FunctionInfo : public NativeInfo { JSValueRef* exception) { FunctionInfo* info = reinterpret_cast(JSObjectGetPrivate(function)); + if (info == nullptr) { + napi_env env = napi_env__::get(const_cast(ctx)); + if (env != nullptr) { + info = NativeInfo::GetNativeInfoKey( + ctx, function, env->function_info_symbol); + } + } + if (info == nullptr) { + return JSValueMakeUndefined(ctx); + } // Make sure any errors encountered last time we were in N-API are gone. napi_clear_last_error(info->_env); @@ -791,13 +830,16 @@ class WrapperInfo : public BaseInfoT { RETURN_STATUS_IF_FALSE(env, IsJSObjectValue(env, object), napi_invalid_arg); WrapperInfo* info{}; - bool hasOwnProperty = NativeInfo::GetNativeInfoKey( - env->context, ToJSObject(env, object), - env->wrapper_info_symbol) != nullptr; + info = GetCached(env, object); + if (info != nullptr) { + *result = info; + return napi_ok; + } - if (hasOwnProperty) { - CHECK_NAPI(Unwrap(env, object, &info)); - RETURN_STATUS_IF_FALSE(env, info != nullptr, napi_generic_failure); + info = NativeInfo::GetNativeInfoKey( + env->context, ToJSObject(env, object), env->wrapper_info_symbol); + if (info != nullptr) { + env->wrapper_info_cache[object] = info; *result = info; return napi_ok; } @@ -811,6 +853,13 @@ class WrapperInfo : public BaseInfoT { NativeInfo::SetNativeInfoKey(env->context, ToJSObject(env, object), info->_class, env->wrapper_info_symbol, info); + info->AddFinalizer([object](WrapperInfo* info) { + napi_env env = info->Env(); + if (env != nullptr) { + env->wrapper_info_cache.erase(object); + } + }); + env->wrapper_info_cache[object] = info; } *result = info; @@ -820,12 +869,37 @@ class WrapperInfo : public BaseInfoT { static napi_status Unwrap(napi_env env, napi_value object, WrapperInfo** result) { RETURN_STATUS_IF_FALSE(env, IsJSObjectValue(env, object), napi_invalid_arg); + auto cachedInfo = GetCached(env, object); + if (cachedInfo != nullptr) { + *result = cachedInfo; + return napi_ok; + } + *result = NativeInfo::GetNativeInfoKey( env->context, ToJSObject(env, object), env->wrapper_info_symbol); + if (*result != nullptr) { + env->wrapper_info_cache[object] = *result; + } return napi_ok; } private: + static WrapperInfo* GetCached(napi_env env, napi_value object) { + auto cachedInfo = env->wrapper_info_cache.find(object); + if (cachedInfo == env->wrapper_info_cache.end()) { + return nullptr; + } + + auto info = NativeInfo::GetNativeInfoKey( + env->context, ToJSObject(env, object), env->wrapper_info_symbol); + if (info == nullptr || info != static_cast(cachedInfo->second)) { + env->wrapper_info_cache.erase(cachedInfo); + return nullptr; + } + + return info; + } + WrapperInfo(napi_env env) : BaseInfoT{env, "Native (Wrapper)"} {} }; @@ -1624,18 +1698,20 @@ napi_status napi_typeof(napi_env env, napi_value value, case kJSTypeString: *result = napi_string; break; - case kJSTypeSymbol: - *result = napi_symbol; - break; - default: - JSObjectRef object{ToJSObject(env, value)}; - if (JSObjectIsFunction(env->context, object)) { - *result = napi_function; - } else { - NativeInfo* info = NativeInfo::Get(object); - if (info != nullptr && info->Type() == NativeType::External) { - *result = napi_external; - } else { + case kJSTypeSymbol: + *result = napi_symbol; + break; + default: + JSObjectRef object{ToJSObject(env, value)}; + NativeInfo* info = NativeInfo::Get(object); + if (JSObjectIsFunction(env->context, object) || + (info != nullptr && (info->Type() == NativeType::Function || + info->Type() == NativeType::Constructor))) { + *result = napi_function; + } else { + if (info != nullptr && info->Type() == NativeType::External) { + *result = napi_external; + } else { *result = napi_object; } } @@ -2104,6 +2180,28 @@ napi_status napi_unwrap(napi_env env, napi_value js_object, void** result) { return napi_ok; } +extern "C" bool nativescript_jsc_try_unwrap_native(napi_env env, + napi_value value, + void** result) { + if (env == nullptr || value == nullptr || result == nullptr) { + return false; + } + + *result = nullptr; + if (!IsJSObjectValue(env, value)) { + return false; + } + + WrapperInfo* info{}; + if (WrapperInfo::Unwrap(env, value, &info) != napi_ok || + info == nullptr || info->Data() == nullptr) { + return false; + } + + *result = info->Data(); + return true; +} + napi_status napi_remove_wrap(napi_env env, napi_value js_object, void** result) { CHECK_ENV(env); diff --git a/NativeScript/napi/jsc/jsc-api.h b/NativeScript/napi/jsc/jsc-api.h index 8aa4b969..36e91f1a 100644 --- a/NativeScript/napi/jsc/jsc-api.h +++ b/NativeScript/napi/jsc/jsc-api.h @@ -10,16 +10,22 @@ #include #include #include +#include #include #include "js_native_api.h" #include "js_native_api_types.h" +extern "C" bool nativescript_jsc_try_unwrap_native(napi_env env, + napi_value value, + void** result); + struct napi_env__ { JSGlobalContextRef context{}; JSValueRef last_exception{}; napi_extended_error_info last_error{nullptr, nullptr, 0, napi_ok}; std::unordered_set active_ref_values{}; + std::unordered_map wrapper_info_cache{}; std::list strong_refs{}; void* instance_data{}; napi_finalize instance_data_finalize_cb; diff --git a/NativeScript/napi/quickjs/quickjs-api.c b/NativeScript/napi/quickjs/quickjs-api.c index 4213296a..d7b23e58 100644 --- a/NativeScript/napi/quickjs/quickjs-api.c +++ b/NativeScript/napi/quickjs/quickjs-api.c @@ -3414,6 +3414,10 @@ napi_status napi_wrap(napi_env env, napi_value js_object, void* native_object, return napi_set_last_error(env, napi_pending_exception, NULL, 0, NULL); } + if (JS_GetClassID(jsValue) == env->runtime->napiObjectClassId) { + JS_SetOpaque(jsValue, externalInfo); + } + if (result) { napi_ref ref; napi_create_reference(env, js_object, 0, &ref); @@ -3434,6 +3438,13 @@ napi_status napi_unwrap(napi_env env, napi_value jsObject, void** result) { return napi_set_last_error(env, napi_object_expected, NULL, 0, NULL); } + ExternalInfo* directInfo = + (ExternalInfo*)JS_GetOpaque(jsValue, env->runtime->napiObjectClassId); + if (directInfo && directInfo->data) { + *result = directInfo->data; + return napi_clear_last_error(env); + } + JSPropertyDescriptor descriptor; int isWrapped = JS_GetOwnProperty(env->context, &descriptor, jsValue, @@ -3496,6 +3507,9 @@ napi_status napi_remove_wrap(napi_env env, napi_value jsObject, void** result) { if (externalInfo) { *result = externalInfo->data; } + if (JS_GetClassID(jsValue) == env->runtime->napiObjectClassId) { + JS_SetOpaque(jsValue, NULL); + } mi_free(externalInfo); JS_SetOpaque(external, NULL); } diff --git a/NativeScript/napi/v8/v8_inspector/Utils.cpp b/NativeScript/napi/v8/v8_inspector/Utils.cpp index e1e6722b..70108a09 100644 --- a/NativeScript/napi/v8/v8_inspector/Utils.cpp +++ b/NativeScript/napi/v8/v8_inspector/Utils.cpp @@ -1,7 +1,7 @@ #include "Utils.h" #include "JsV8InspectorClient.h" -#include "Util.h" +#include "ffi/napi/Util.h" using namespace v8; using namespace std; diff --git a/NativeScript/napi/v8/v8_inspector/ns-v8-tracing-agent-impl.cpp b/NativeScript/napi/v8/v8_inspector/ns-v8-tracing-agent-impl.cpp index f8a1da91..a871a7fb 100644 --- a/NativeScript/napi/v8/v8_inspector/ns-v8-tracing-agent-impl.cpp +++ b/NativeScript/napi/v8/v8_inspector/ns-v8-tracing-agent-impl.cpp @@ -14,7 +14,7 @@ #include #include "JsV8InspectorClient.h" -#include "NativeScriptException.h" +#include "runtime/NativeScriptException.h" #include "Runtime.h" namespace tns { diff --git a/NativeScript/runtime/NativeScript.mm b/NativeScript/runtime/NativeScript.mm index 20d0689b..827e0612 100644 --- a/NativeScript/runtime/NativeScript.mm +++ b/NativeScript/runtime/NativeScript.mm @@ -1,8 +1,8 @@ #include "NativeScript.h" #include "Runtime.h" #include "RuntimeConfig.h" -#include "ffi/NativeScriptException.h" -#include "ffi/Tasks.h" +#include "runtime/NativeScriptException.h" +#include "ffi/shared/Tasks.h" #include "js_native_api.h" #include "jsr.h" diff --git a/NativeScript/ffi/NativeScriptException.h b/NativeScript/runtime/NativeScriptException.h similarity index 89% rename from NativeScript/ffi/NativeScriptException.h rename to NativeScript/runtime/NativeScriptException.h index 3f973009..c46488d9 100644 --- a/NativeScript/ffi/NativeScriptException.h +++ b/NativeScript/runtime/NativeScriptException.h @@ -1,5 +1,5 @@ -#ifndef NativeScriptException_h -#define NativeScriptException_h +#ifndef NATIVESCRIPT_RUNTIME_NATIVE_SCRIPT_EXCEPTION_H +#define NATIVESCRIPT_RUNTIME_NATIVE_SCRIPT_EXCEPTION_H #include @@ -49,4 +49,4 @@ class NativeScriptException { } // namespace nativescript -#endif /* NativeScriptException_h */ +#endif // NATIVESCRIPT_RUNTIME_NATIVE_SCRIPT_EXCEPTION_H diff --git a/NativeScript/ffi/NativeScriptException.mm b/NativeScript/runtime/NativeScriptException.mm similarity index 99% rename from NativeScript/ffi/NativeScriptException.mm rename to NativeScript/runtime/NativeScriptException.mm index 6b9a01be..6fe9e178 100644 --- a/NativeScript/ffi/NativeScriptException.mm +++ b/NativeScript/runtime/NativeScriptException.mm @@ -1,4 +1,4 @@ -#include "NativeScriptException.h" +#include "runtime/NativeScriptException.h" #import #include #include "js_native_api.h" diff --git a/NativeScript/runtime/Runtime.cpp b/NativeScript/runtime/Runtime.cpp index 7ba14da0..56cc4529 100644 --- a/NativeScript/runtime/Runtime.cpp +++ b/NativeScript/runtime/Runtime.cpp @@ -8,11 +8,24 @@ #include "js_native_api_types.h" #include "jsr.h" #include "jsr_common.h" +#include "runtime/Util.h" #include "runtime/modules/RuntimeModules.h" #ifdef TARGET_ENGINE_V8 +#include "ffi/v8/NativeApiV8.h" #include "v8-api.h" #endif // TARGET_ENGINE_V8 +#ifdef TARGET_ENGINE_HERMES +#include "ffi/hermes/jsi/NativeApiJsi.h" +#endif // TARGET_ENGINE_HERMES +#ifdef TARGET_ENGINE_JSC +#include "ffi/jsc/NativeApiJSC.h" +#endif // TARGET_ENGINE_JSC +#ifdef TARGET_ENGINE_QUICKJS +#include "ffi/quickjs/NativeApiQuickJS.h" +#endif // TARGET_ENGINE_QUICKJS #include +#include +#include #include "NativeScript.h" #include "robin_hood.h" @@ -39,6 +52,53 @@ Runtime* Runtime::GetRuntime(napi_env env) { return nullptr; } +#ifdef TARGET_ENGINE_HERMES +class HermesRuntimeUnlockScope final { + public: + explicit HermesRuntimeUnlockScope(napi_env env) { + auto it = JSR::env_to_jsr_cache.find(env); + jsr_ = it != JSR::env_to_jsr_cache.end() ? it->second : nullptr; + if (jsr_ == nullptr) { + return; + } + + unlockedDepth_ = js_current_env_lock_depth(env); + for (int i = 0; i < unlockedDepth_; i++) { + jsr_->unlock(); + } + if (unlockedDepth_ == 0 && jsr_->runtime != nullptr) { + jsr_->runtime->unlock(); + unlockedRuntime_ = true; + } + } + + ~HermesRuntimeUnlockScope() { + if (unlockedRuntime_ && jsr_ != nullptr && jsr_->runtime != nullptr) { + jsr_->runtime->lock(); + } + if (jsr_ != nullptr) { + for (int i = 0; i < unlockedDepth_; i++) { + jsr_->lock(); + } + } + } + + HermesRuntimeUnlockScope(const HermesRuntimeUnlockScope&) = delete; + HermesRuntimeUnlockScope& operator=(const HermesRuntimeUnlockScope&) = delete; + + private: + JSR* jsr_ = nullptr; + int unlockedDepth_ = 0; + bool unlockedRuntime_ = false; +}; + +void InvokeWithUnlockedHermesRuntime(napi_env env, + const std::function& task) { + HermesRuntimeUnlockScope scope(env); + task(); +} +#endif // TARGET_ENGINE_HERMES + Runtime::Runtime() { currentRuntime_ = this; workerId_ = -1; @@ -113,6 +173,26 @@ void Runtime::Init(bool isWorker) { napi_set_named_property(env_, global, "global", global); const char* CompatScript = R"( + if (typeof globalThis.__extends !== "function") { + var __extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function(d, b) { d.__proto__ = b; }) || + function(d, b) { + for (var p in b) { + if (Object.prototype.hasOwnProperty.call(b, p)) { + d[p] = b[p]; + } + } + }; + globalThis.__extends = function(d, b) { + if (typeof b !== "function" && b !== null) { + throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); + } + __extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; + } + if (typeof globalThis.__decorate !== "function") { globalThis.__decorate = function(decorators, target, key, desc) { var c = arguments.length; @@ -260,10 +340,51 @@ void Runtime::Init(bool isWorker) { // Ensure that Promise callbacks are executed on the // same thread on which they were created (() => { + const runLoopQueues = []; + + function getRunLoopQueue(runloop) { + for (let i = 0; i < runLoopQueues.length; i++) { + if (runLoopQueues[i].runloop === runloop) { + return runLoopQueues[i]; + } + } + + const queue = { + runloop, + pending: false, + callbacks: [], + drain() { + queue.pending = false; + const callbacks = queue.callbacks.splice(0); + for (let i = 0; i < callbacks.length; i++) { + callbacks[i](); + } + if (queue.callbacks.length > 0 && !queue.pending) { + queue.pending = true; + CFRunLoopPerformBlock(queue.runloop, kCFRunLoopDefaultMode, queue.drain); + CFRunLoopWakeUp(queue.runloop); + } + } + }; + runLoopQueues.push(queue); + return queue; + } + + function scheduleOnRunLoop(queue, callback) { + queue.callbacks.push(callback); + if (queue.pending) { + return; + } + queue.pending = true; + CFRunLoopPerformBlock(queue.runloop, kCFRunLoopDefaultMode, queue.drain); + CFRunLoopWakeUp(queue.runloop); + } + global.Promise = new Proxy(global.Promise, { construct: function(target, args) { let origFunc = args[0]; let runloop = CFRunLoopGetCurrent(); + let runloopQueue = getRunLoopQueue(runloop); let promise = new target(function(resolve, reject) { function isFulfilled() { @@ -278,27 +399,31 @@ void Runtime::Init(bool isWorker) { if (isFulfilled()) { return; } - const resolveCall = resolve.bind(this, value); + const resolveFn = resolve; + const resolveCall = function() { + resolveFn(value); + }; if (runloop === CFRunLoopGetCurrent()) { markFulfilled(); resolveCall(); } else { - CFRunLoopPerformBlock(runloop, kCFRunLoopDefaultMode, resolveCall); - CFRunLoopWakeUp(runloop); markFulfilled(); + scheduleOnRunLoop(runloopQueue, resolveCall); } }, reason => { if (isFulfilled()) { return; } - const rejectCall = reject.bind(this, reason); + const rejectFn = reject; + const rejectCall = function() { + rejectFn(reason); + }; if (runloop === CFRunLoopGetCurrent()) { markFulfilled(); rejectCall(); } else { - CFRunLoopPerformBlock(runloop, kCFRunLoopDefaultMode, rejectCall); - CFRunLoopWakeUp(runloop); markFulfilled(); + scheduleOnRunLoop(runloopQueue, rejectCall); } }); }); @@ -359,7 +484,113 @@ void Runtime::Init(bool isWorker) { #endif const char* metadata_path = std::getenv("NS_METADATA_PATH"); +#if NS_FFI_BACKEND_NAPI nativescript_init(env_, metadata_path, RuntimeConfig.MetadataPtr); +#endif + +#if NS_FFI_BACKEND_DIRECT && defined(TARGET_ENGINE_V8) + { + NativeApiV8Config nativeApiV8Config; + nativeApiV8Config.metadataPath = metadata_path; + nativeApiV8Config.metadataPtr = RuntimeConfig.MetadataPtr; + nativeApiV8Config.installGlobalSymbols = true; + nativeApiV8Config.nativeCallbackInvoker = + [env = env_](std::function task) { + NapiScope scope(env); + task(); + }; + nativeApiV8Config.jsThreadCallbackInvoker = + [env = env_, runLoop = runtimeLoop_](std::function task) { + ExecuteOnRunLoop( + runLoop, + [env, task = std::move(task)]() mutable { + NapiScope scope(env); + task(); + }, + false); + }; + InstallNativeApiV8(env_->isolate, env_->context(), nativeApiV8Config); + } +#endif // NS_FFI_BACKEND_DIRECT && TARGET_ENGINE_V8 + +#if NS_FFI_BACKEND_DIRECT && defined(TARGET_ENGINE_HERMES) + if (auto* jsiRuntime = js_get_jsi_runtime(env_)) { + NativeApiJsiConfig nativeApiJsiConfig; + nativeApiJsiConfig.metadataPath = metadata_path; + nativeApiJsiConfig.metadataPtr = RuntimeConfig.MetadataPtr; + nativeApiJsiConfig.installGlobalSymbols = true; + nativeApiJsiConfig.nativeInvocationInvoker = + [env = env_](std::function task) { + InvokeWithUnlockedHermesRuntime(env, task); + }; + nativeApiJsiConfig.nativeCallbackInvoker = + [env = env_](std::function task) { + NapiScope scope(env); + task(); + }; + nativeApiJsiConfig.jsThreadCallbackInvoker = + [env = env_, runLoop = runtimeLoop_](std::function task) { + ExecuteOnRunLoop( + runLoop, + [env, task = std::move(task)]() mutable { + NapiScope scope(env); + task(); + }, + false); + }; + InstallNativeApiJSI(*jsiRuntime, nativeApiJsiConfig); + } +#endif // NS_FFI_BACKEND_DIRECT && TARGET_ENGINE_HERMES + +#if NS_FFI_BACKEND_DIRECT && defined(TARGET_ENGINE_JSC) + { + NativeApiJSCConfig nativeApiJSCConfig; + nativeApiJSCConfig.metadataPath = metadata_path; + nativeApiJSCConfig.metadataPtr = RuntimeConfig.MetadataPtr; + nativeApiJSCConfig.installGlobalSymbols = true; + nativeApiJSCConfig.nativeCallbackInvoker = + [env = env_](std::function task) { + NapiScope scope(env); + task(); + }; + nativeApiJSCConfig.jsThreadCallbackInvoker = + [env = env_, runLoop = runtimeLoop_](std::function task) { + ExecuteOnRunLoop( + runLoop, + [env, task = std::move(task)]() mutable { + NapiScope scope(env); + task(); + }, + false); + }; + InstallNativeApiJSC(env_->context, nativeApiJSCConfig); + } +#endif // NS_FFI_BACKEND_DIRECT && TARGET_ENGINE_JSC + +#if NS_FFI_BACKEND_DIRECT && defined(TARGET_ENGINE_QUICKJS) + { + NativeApiQuickJSConfig nativeApiQuickJSConfig; + nativeApiQuickJSConfig.metadataPath = metadata_path; + nativeApiQuickJSConfig.metadataPtr = RuntimeConfig.MetadataPtr; + nativeApiQuickJSConfig.installGlobalSymbols = true; + nativeApiQuickJSConfig.nativeCallbackInvoker = + [env = env_](std::function task) { + NapiScope scope(env); + task(); + }; + nativeApiQuickJSConfig.jsThreadCallbackInvoker = + [env = env_, runLoop = runtimeLoop_](std::function task) { + ExecuteOnRunLoop( + runLoop, + [env, task = std::move(task)]() mutable { + NapiScope scope(env); + task(); + }, + false); + }; + InstallNativeApiQuickJS(qjs_get_context(env_), nativeApiQuickJSConfig); + } +#endif // NS_FFI_BACKEND_DIRECT && TARGET_ENGINE_QUICKJS napi_close_handle_scope(env_, scope); } diff --git a/NativeScript/runtime/Util.h b/NativeScript/runtime/Util.h index 7699cde9..7d5ed072 100644 --- a/NativeScript/runtime/Util.h +++ b/NativeScript/runtime/Util.h @@ -1,6 +1,6 @@ #include -#include "ffi/NativeScriptException.h" +#include "runtime/NativeScriptException.h" #include "jsr_common.h" #include "native_api_util.h" #include "robin_hood.h" diff --git a/NativeScript/runtime/modules/console/Console.cpp b/NativeScript/runtime/modules/console/Console.cpp index fa7ae1ff..f8a07a22 100644 --- a/NativeScript/runtime/modules/console/Console.cpp +++ b/NativeScript/runtime/modules/console/Console.cpp @@ -2,8 +2,9 @@ #include #include +#include -#include "ffi/NativeScriptException.h" +#include "runtime/NativeScriptException.h" #include "js_native_api.h" #include "native_api_util.h" #include "runtime/RuntimeConfig.h" @@ -34,6 +35,64 @@ extern "C" void NSLog(CFStringRef format, ...); namespace nativescript { +namespace { + +bool readStringValue(napi_env env, napi_value value, std::string& result) { + if (value == nullptr) { + return false; + } + + size_t length = 0; + if (napi_get_value_string_utf8(env, value, nullptr, 0, &length) != napi_ok) { + return false; + } + + std::vector buffer(length + 1); + size_t copied = 0; + if (napi_get_value_string_utf8(env, value, buffer.data(), buffer.size(), + &copied) != napi_ok) { + return false; + } + + result.assign(buffer.data(), copied); + return true; +} + +bool throwPendingException(napi_env env, const std::string& message) { + bool isPending = false; + if (napi_is_exception_pending(env, &isPending) != napi_ok || !isPending) { + return false; + } + + napi_value exception = nullptr; + napi_get_and_clear_last_exception(env, &exception); + + if (exception != nullptr && + napi_util::is_of_type(env, exception, napi_object)) { + throw NativeScriptException(env, exception, message); + } + + throw NativeScriptException(env, message); +} + +bool coerceToString(napi_env env, napi_value value, std::string& result) { + napi_value stringValue = nullptr; + if (napi_coerce_to_string(env, value, &stringValue) != napi_ok || + stringValue == nullptr) { + throwPendingException(env, "Error converting console argument to string"); + return false; + } + + if (!readStringValue(env, stringValue, result)) { + throwPendingException(env, "Error reading console argument string"); + return false; + } + + return true; +} + +} // namespace + JS_CLASS_INIT(Console::Init) { napi_value Console, console; @@ -65,17 +124,38 @@ JS_METHOD(Console::Constructor) { } std::string transformJSObject(napi_env env, napi_value object) { - napi_value toStringFunc; + napi_value toStringFunc = nullptr; bool hasToString = false; // Check if the object has a toString method - napi_has_named_property(env, object, "toString", &hasToString); + if (napi_has_named_property(env, object, "toString", &hasToString) != + napi_ok) { + throwPendingException(env, "Error reading console object toString"); + return "[object Object]"; + } + if (hasToString) { - napi_get_named_property(env, object, "toString", &toStringFunc); - if (napi_util::is_of_type(env, toStringFunc, napi_function)) { - napi_value result; - napi_call_function(env, object, toStringFunc, 0, nullptr, &result); - auto value = napi_util::get_cxx_string(env, result); + if (napi_get_named_property(env, object, "toString", &toStringFunc) != + napi_ok) { + throwPendingException(env, "Error reading console object toString"); + return "[object Object]"; + } + + if (toStringFunc != nullptr && + napi_util::is_of_type(env, toStringFunc, napi_function)) { + napi_value result = nullptr; + if (napi_call_function(env, object, toStringFunc, 0, nullptr, &result) != + napi_ok || + result == nullptr) { + throwPendingException(env, "Error converting console object to string"); + return "[object Object]"; + } + + std::string value; + if (!coerceToString(env, result, value)) { + return "[object Object]"; + } + auto hasCustomToStringImplementation = value.find("[object Object]") == std::string::npos; if (hasCustomToStringImplementation) return value; @@ -88,29 +168,48 @@ std::string transformJSObject(napi_env env, napi_value object) { std::string buildStringFromArg(napi_env env, napi_value val, napi_value inspectSymbol) { napi_valuetype type; - napi_typeof(env, val, &type); + if (napi_typeof(env, val, &type) != napi_ok) { + throwPendingException(env, "Error reading console argument type"); + return ""; + } if (type == napi_function) { - napi_value funcString; - napi_coerce_to_string(env, val, &funcString); - return napi_util::get_string_value(env, funcString); + std::string funcString; + if (coerceToString(env, val, funcString)) { + return funcString; + } + return ""; } else if (napi_util::is_array(env, val)) { napi_value cachedSelf = val; // Get array length - uint32_t arrayLength; - napi_get_array_length(env, val, &arrayLength); + uint32_t arrayLength = 0; + if (napi_get_array_length(env, val, &arrayLength) != napi_ok) { + throwPendingException(env, "Error reading console array length"); + return "[]"; + } std::stringstream arrayStr; arrayStr << "["; for (uint32_t i = 0; i < arrayLength; i++) { - napi_value propertyValue; - napi_get_element(env, val, i, &propertyValue); + napi_value propertyValue = nullptr; + if (napi_get_element(env, val, i, &propertyValue) != napi_ok || + propertyValue == nullptr) { + throwPendingException(env, "Error reading console array element"); + arrayStr << ""; + if (i != arrayLength - 1) { + arrayStr << ", "; + } + continue; + } // Check for circular reference bool isStrictEqual = false; - napi_strict_equals(env, propertyValue, cachedSelf, &isStrictEqual); + if (napi_strict_equals(env, propertyValue, cachedSelf, &isStrictEqual) != + napi_ok) { + throwPendingException(env, "Error comparing console array element"); + } if (isStrictEqual) { arrayStr << "[Circular]"; @@ -132,20 +231,32 @@ std::string buildStringFromArg(napi_env env, napi_value val, napi_status getInspectStatus = napi_get_property(env, val, inspectSymbol, &inspectFunc); if (getInspectStatus == napi_ok && + inspectFunc != nullptr && napi_util::is_of_type(env, inspectFunc, napi_function)) { - napi_value inspectedValue; - napi_call_function(env, val, inspectFunc, 0, nullptr, &inspectedValue); + napi_value inspectedValue = nullptr; + if (napi_call_function(env, val, inspectFunc, 0, nullptr, + &inspectedValue) != napi_ok || + inspectedValue == nullptr) { + throwPendingException(env, "Error inspecting console object"); + return "[object Object]"; + } return buildStringFromArg(env, inspectedValue, inspectSymbol); + } else if (getInspectStatus != napi_ok) { + throwPendingException(env, "Error reading console inspect function"); } return transformJSObject(env, val); } else if (type == napi_symbol) { - napi_value symString; - napi_coerce_to_string(env, val, &symString); - return "Symbol(" + napi_util::get_cxx_string(env, symString) + ")"; + std::string symString; + if (coerceToString(env, val, symString)) { + return symString; + } + return "Symbol()"; } else { - napi_value defaultToString; - napi_coerce_to_string(env, val, &defaultToString); - return napi_util::get_string_value(env, defaultToString); + std::string defaultToString; + if (coerceToString(env, val, defaultToString)) { + return defaultToString; + } + return ""; } } diff --git a/NativeScript/runtime/modules/module/ModuleInternal.cpp b/NativeScript/runtime/modules/module/ModuleInternal.cpp index 993bed31..efec4bc9 100644 --- a/NativeScript/runtime/modules/module/ModuleInternal.cpp +++ b/NativeScript/runtime/modules/module/ModuleInternal.cpp @@ -15,7 +15,7 @@ #include #include -#include "ffi/NativeScriptException.h" +#include "runtime/NativeScriptException.h" #include "native_api_util.h" #include "runtime/RuntimeConfig.h" #include "runtime/Util.h" diff --git a/NativeScript/runtime/modules/node/Process.cpp b/NativeScript/runtime/modules/node/Process.cpp index 5a0b23fc..ef77ba05 100644 --- a/NativeScript/runtime/modules/node/Process.cpp +++ b/NativeScript/runtime/modules/node/Process.cpp @@ -186,6 +186,9 @@ napi_value CreateVersionsObject(napi_env env) { #elif defined(TARGET_ENGINE_QUICKJS) napi_set_named_property(env, versions, "engine", napi_util::to_js_string(env, "quickjs")); +#elif defined(TARGET_ENGINE_JSC) + napi_set_named_property(env, versions, "engine", + napi_util::to_js_string(env, "jsc")); #endif napi_set_named_property(env, versions, "nativescript", diff --git a/NativeScript/runtime/modules/timers/Timers.mm b/NativeScript/runtime/modules/timers/Timers.mm index c5800a5f..f9fe7a07 100644 --- a/NativeScript/runtime/modules/timers/Timers.mm +++ b/NativeScript/runtime/modules/timers/Timers.mm @@ -9,6 +9,7 @@ #include #include #include "Timers.h" +#include "ffi/napi/CallbackThreading.h" static std::atomic gActiveTimers{0}; struct TimerToken; @@ -192,7 +193,55 @@ void MarkTimerActive(NSTimerHandle* handle) { } } -void AddTimerToMainRunLoop(NSTimer* timer) { +bool MarkTimerInactive(NSTimerHandle* handle) { + if (handle == nil) { + return false; + } + + bool didDeactivate = false; + @synchronized(handle) { + if (handle->activeCounted) { + handle->activeCounted = false; + didDeactivate = true; + } + } + + if (didDeactivate) { + gActiveTimers.fetch_sub(1, std::memory_order_relaxed); + } + + return didDeactivate; +} + +bool shouldAvoidMainQueueSyncWhileHoldingHermesLock(napi_env env) { +#ifdef TARGET_ENGINE_HERMES + if ([NSThread isMainThread]) { + return false; + } + + // A native-caller-thread callback already owns the Hermes JS lock. A + // synchronous hop to main can deadlock if the main run loop is draining jobs. + return nativescript::isNativeCallerThreadCallbackActive() || + (env != nullptr && js_current_env_lock_depth(env) > 0); +#else + (void)env; + return false; +#endif +} + +void DrainPendingJobs(napi_env env) { + if (env == nullptr) { + return; + } + +#ifdef ENABLE_JS_RUNTIME + js_execute_pending_jobs(env); +#else + (void)env; +#endif +} + +void AddTimerToMainRunLoop(napi_env env, NSTimer* timer) { if (timer == nil) { return; } @@ -208,6 +257,15 @@ void AddTimerToMainRunLoop(NSTimer* timer) { return; } + if (shouldAvoidMainQueueSyncWhileHoldingHermesLock(env)) { + [timer retain]; + dispatch_async(dispatch_get_main_queue(), ^{ + addTimer(); + [timer release]; + }); + return; + } + dispatch_sync(dispatch_get_main_queue(), addTimer); } @@ -234,24 +292,24 @@ void DisposeTimerHandle(napi_env callEnv, NSTimerHandle* handle, bool invalidate if ([NSThread isMainThread]) { disposeTimer(); + } else if (shouldAvoidMainQueueSyncWhileHoldingHermesLock( + callEnv != nullptr ? callEnv : handle->env)) { + [handle retain]; + dispatch_async(dispatch_get_main_queue(), ^{ + disposeTimer(); + [handle release]; + }); } else { dispatch_sync(dispatch_get_main_queue(), disposeTimer); } napi_ref callback = nullptr; - bool shouldDecrementActiveCount = false; @synchronized(handle) { - if (handle->activeCounted) { - handle->activeCounted = false; - shouldDecrementActiveCount = true; - } callback = handle->callback; handle->callback = nullptr; } - if (shouldDecrementActiveCount) { - gActiveTimers.fetch_sub(1, std::memory_order_relaxed); - } + MarkTimerInactive(handle); napi_env cleanupEnv = callEnv != nullptr ? callEnv : handle->env; #ifdef TARGET_ENGINE_HERMES @@ -264,9 +322,7 @@ void DisposeTimerHandle(napi_env callEnv, NSTimerHandle* handle, bool invalidate uint32_t remaining = 0; napi_reference_unref(cleanupEnv, callback, &remaining); napi_delete_reference(cleanupEnv, callback); -#ifdef TARGET_ENGINE_HERMES - js_execute_pending_jobs(cleanupEnv); -#endif + DrainPendingJobs(cleanupEnv); } } @@ -275,6 +331,11 @@ void ScheduleOneShotTimerCleanup(napi_env env, NSTimerHandle* handle) { return; } + if ([NSThread isMainThread]) { + DisposeTimerHandle(env, handle, false); + return; + } + [handle retain]; dispatch_async(dispatch_get_main_queue(), ^{ DisposeTimerHandle(env, handle, false); @@ -370,6 +431,7 @@ void ScheduleOneShotTimerCleanup(napi_env env, NSTimerHandle* handle) { } NapiScope scope(callbackEnv); + MarkTimerInactive(handle); #ifdef TARGET_ENGINE_HERMES DispatchHermesTimerCallback(callbackEnv, "__nsDispatchTimeout", timerId); #else @@ -382,9 +444,7 @@ void ScheduleOneShotTimerCleanup(napi_env env, NSTimerHandle* handle) { napi_get_reference_value(callbackEnv, callbackRef, &callbackValue); napi_call_function(callbackEnv, global, callbackValue, 0, nullptr, nullptr); #endif -#ifdef TARGET_ENGINE_HERMES - js_execute_pending_jobs(callbackEnv); -#endif + DrainPendingJobs(callbackEnv); ScheduleOneShotTimerCleanup(callbackEnv, handle); [handle release]; @@ -413,7 +473,7 @@ void ScheduleOneShotTimerCleanup(napi_env env, NSTimerHandle* handle) { // Drop creator ownership. Remaining ownership is the timer association. [handle release]; - AddTimerToMainRunLoop(timer); + AddTimerToMainRunLoop(env, timer); return result; } @@ -486,9 +546,7 @@ void ScheduleOneShotTimerCleanup(napi_env env, NSTimerHandle* handle) { napi_get_reference_value(callbackEnv, callbackRef, &callbackValue); napi_call_function(callbackEnv, global, callbackValue, 0, nullptr, nullptr); #endif -#ifdef TARGET_ENGINE_HERMES - js_execute_pending_jobs(callbackEnv); -#endif + DrainPendingJobs(callbackEnv); [handle release]; }]; @@ -515,7 +573,7 @@ void ScheduleOneShotTimerCleanup(napi_env env, NSTimerHandle* handle) { // Drop creator ownership. Remaining ownership is the timer association. [handle release]; - AddTimerToMainRunLoop(timer); + AddTimerToMainRunLoop(env, timer); return result; } diff --git a/NativeScript/runtime/modules/worker/MessageV8.cpp b/NativeScript/runtime/modules/worker/MessageV8.cpp index 183149d8..f32c97c4 100644 --- a/NativeScript/runtime/modules/worker/MessageV8.cpp +++ b/NativeScript/runtime/modules/worker/MessageV8.cpp @@ -10,7 +10,7 @@ #include "MessageV8.h" -#include "ffi/NativeScriptException.h" +#include "runtime/NativeScriptException.h" #include "v8-api.h" using namespace v8; diff --git a/NativeScript/runtime/modules/worker/Worker.mm b/NativeScript/runtime/modules/worker/Worker.mm index ce39e2ad..039f86f0 100644 --- a/NativeScript/runtime/modules/worker/Worker.mm +++ b/NativeScript/runtime/modules/worker/Worker.mm @@ -2,7 +2,7 @@ #include #include #include -#include "ffi/NativeScriptException.h" +#include "runtime/NativeScriptException.h" #include "js_native_api.h" #include "js_native_api_types.h" #include "jsr.h" diff --git a/NativeScript/runtime/modules/worker/WorkerImpl.h b/NativeScript/runtime/modules/worker/WorkerImpl.h index 842c81a8..90c23d01 100644 --- a/NativeScript/runtime/modules/worker/WorkerImpl.h +++ b/NativeScript/runtime/modules/worker/WorkerImpl.h @@ -5,7 +5,7 @@ #include #include -#include "ffi/NativeScriptException.h" +#include "runtime/NativeScriptException.h" #include "js_native_api_types.h" #include "native_api_util.h" #include "runtime/modules/worker/ConcurrentMap.h" diff --git a/NativeScript/runtime/modules/worker/WorkerImpl.mm b/NativeScript/runtime/modules/worker/WorkerImpl.mm index aba8b4e4..4fa77f34 100644 --- a/NativeScript/runtime/modules/worker/WorkerImpl.mm +++ b/NativeScript/runtime/modules/worker/WorkerImpl.mm @@ -1,7 +1,7 @@ #include #include #include -#include "ffi/NativeScriptException.h" +#include "runtime/NativeScriptException.h" #include "js_native_api.h" #include "js_native_api_types.h" #include "jsr.h" diff --git a/benchmarks/objc-dispatch/README.md b/benchmarks/objc-dispatch/README.md new file mode 100644 index 00000000..6a2951bd --- /dev/null +++ b/benchmarks/objc-dispatch/README.md @@ -0,0 +1,45 @@ +# Objective-C Dispatch Benchmarks + +This benchmark compares hot Objective-C dispatch shapes across the generated +signature dispatch runtime and the PR #366 AOT direct-call runtime. + +The benchmark body is plain NativeScript JavaScript: + +- `objc-dispatch-benchmarks.js` + +The runner can execute it in three modes: + +- `napi-node`: fastest smoke run using the packaged macOS Node-API runtime. +- `napi-ios`: builds a temporary iOS app from the packaged `@nativescript/ios` + template and runs it in Simulator. +- `legacy-ios`: temporarily injects the benchmark into the PR branch + `TestRunner` app, builds it, runs it in Simulator, then restores the app + entry point. + +For V8 builds, `gsd-off` still uses the V8-native callback/marshalling path, +but generated signature dispatch lookup is disabled so Objective-C calls fall +back to the dynamic prepared/`ffi_call` path. This keeps the comparison focused +on the generated dispatch win instead of accidentally measuring a hand-written +direct `objc_msgSend` fast path. + +For JSC, QuickJS, and Hermes builds, `gsd-off` follows the same rule: the +engine-native callback and marshalling layer remains active, while only the +generated typed invoker lookup is disabled. + +Examples: + +```sh +npm run benchmark:objc-dispatch -- --runtime napi-node --iterations 100000 +npm run benchmark:objc-dispatch -- --runtime napi-ios,legacy-ios --iterations 250000 +npm run benchmark:objc-dispatch -- --runtime all --include-napi-gsd-off +``` + +Useful options: + +```sh +--legacy-repo /path/to/NativeScript/ios +--destination "platform=iOS Simulator,id=" +--napi-package-tgz /path/to/nativescript-ios.tgz +--iterations 250000 +--include-napi-gsd-off +``` diff --git a/benchmarks/objc-dispatch/objc-dispatch-benchmarks.js b/benchmarks/objc-dispatch/objc-dispatch-benchmarks.js new file mode 100644 index 00000000..1778a344 --- /dev/null +++ b/benchmarks/objc-dispatch/objc-dispatch-benchmarks.js @@ -0,0 +1,215 @@ +(function () { + "use strict"; + + var marker = "NS_BENCH_RESULT:"; + var runtime = globalThis.__NS_BENCHMARK_RUNTIME || "unknown"; + var variant = globalThis.__NS_BENCHMARK_VARIANT || "default"; + var options = globalThis.__NS_BENCHMARK_OPTIONS__ || {}; + var baseIterations = Math.max(1, Number(options.iterations || 250000) | 0); + var warmupIterations = Math.max(0, Number(options.warmupIterations || Math.min(10000, baseIterations / 10)) | 0); + var sink = 0; + + function nowMs() { + if (globalThis.performance && typeof globalThis.performance.now === "function") { + return globalThis.performance.now(); + } + return Date.now(); + } + + function consume(value) { + var n = 0; + switch (typeof value) { + case "number": + n = value | 0; + break; + case "boolean": + n = value ? 1 : 0; + break; + case "string": + n = value.length; + break; + case "object": + case "function": + if (value === null || value === undefined) { + n = 0; + } else if (typeof value.length === "number") { + n = value.length | 0; + } else if (typeof value.count === "number") { + n = value.count | 0; + } else { + n = 1; + } + break; + default: + n = value ? 1 : 0; + break; + } + + sink = ((sink << 5) - sink + n) | 0; + } + + function runLoop(iterations, fn) { + for (var i = 0; i < iterations; i++) { + consume(fn(i)); + } + } + + function bench(name, factor, fn) { + var iterations = Math.max(1, Math.floor(baseIterations * factor)); + var warmup = Math.min(warmupIterations, iterations); + runLoop(warmup, fn); + + var started = nowMs(); + runLoop(iterations, fn); + var elapsedMs = nowMs() - started; + + return { + name: name, + iterations: iterations, + ms: elapsedMs, + nsPerOp: elapsedMs * 1000000 / iterations + }; + } + + function emit(payload) { + console.log(marker + JSON.stringify(payload)); + } + + function addCase(cases, name, factor, fn) { + try { + consume(fn(0)); + cases.push({ name: name, factor: factor, fn: fn }); + } catch (error) { + cases.push({ + name: name, + skip: true, + error: error && error.message ? error.message : String(error) + }); + } + } + + function buildCases() { + var cases = []; + var object = NSObject.alloc().init(); + var otherObject = NSObject.alloc().init(); + var string = NSString.stringWithString("NativeScript dispatch benchmark"); + var compareString = NSString.stringWithString("NativeScript dispatch baseline"); + var prefix = NSString.stringWithString("NativeScript"); + var key = NSString.stringWithString("benchmark-key"); + var array = NSMutableArray.alloc().init(); + array.addObject(object); + array.addObject(otherObject); + array.addObject(string); + + var immutableArray = NSArray.arrayWithArray([object, otherObject, string, object]); + var dictionary = NSMutableDictionary.alloc().init(); + var date = NSDate.dateWithTimeIntervalSince1970(123456); + + addCase(cases, "js.loop.baseline", 1, function (i) { + return i; + }); + + addCase(cases, "NSObject.respondsToSelector", 1, function () { + return object.respondsToSelector("description"); + }); + + addCase(cases, "NSObject.isKindOfClass", 1, function () { + return object.isKindOfClass(NSObject); + }); + + addCase(cases, "NSObject.description.getter", 0.25, function () { + return object.description; + }); + + addCase(cases, "NSObject.hash.getter", 1, function () { + return object.hash; + }); + + addCase(cases, "NSString.length.getter", 1, function () { + return string.length; + }); + + addCase(cases, "NSString.characterAtIndex", 1, function (i) { + return string.characterAtIndex(i & 7); + }); + + addCase(cases, "NSString.compare", 1, function () { + return string.compare(compareString); + }); + + addCase(cases, "NSString.hasPrefix", 1, function () { + return string.hasPrefix(prefix); + }); + + addCase(cases, "NSArray.objectAtIndex", 1, function (i) { + return immutableArray.objectAtIndex(i & 3); + }); + + addCase(cases, "NSMutableArray.count.getter", 1, function () { + return array.count; + }); + + addCase(cases, "NSMutableArray.addRemoveObject", 0.5, function () { + array.addObject(object); + array.removeObjectAtIndex(array.count - 1); + return array.count; + }); + + addCase(cases, "NSMutableDictionary.setRemoveObject", 0.5, function () { + dictionary.setObjectForKey(object, key); + dictionary.removeObjectForKey(key); + return dictionary.count; + }); + + addCase(cases, "NSDate.timeIntervalSince1970", 1, function () { + return date.timeIntervalSince1970; + }); + + if (typeof CGPointMake === "function") { + addCase(cases, "CoreGraphics.CGPointMake", 0.5, function (i) { + return CGPointMake(i & 255, (i + 1) & 255).x; + }); + } + + return cases; + } + + var startedAt = nowMs(); + var results = []; + var skipped = []; + var cases = buildCases(); + + for (var i = 0; i < cases.length; i++) { + var item = cases[i]; + if (item.skip) { + var skippedCase = { name: item.name, error: item.error }; + skipped.push(skippedCase); + emit({ kind: "skip", name: skippedCase.name, error: skippedCase.error }); + continue; + } + var result = bench(item.name, item.factor, item.fn); + results.push(result); + emit({ + kind: "case", + name: result.name, + iterations: result.iterations, + ms: result.ms, + nsPerOp: result.nsPerOp + }); + } + + var report = { + kind: "done", + version: 1, + runtime: runtime, + variant: variant, + baseIterations: baseIterations, + warmupIterations: warmupIterations, + totalMs: nowMs() - startedAt, + sink: sink, + resultCount: results.length, + skippedCount: skipped.length + }; + + emit(report); +}()); diff --git a/benchmarks/objc-dispatch/run.js b/benchmarks/objc-dispatch/run.js new file mode 100644 index 00000000..7d8be2dc --- /dev/null +++ b/benchmarks/objc-dispatch/run.js @@ -0,0 +1,955 @@ +#!/usr/bin/env node +"use strict"; + +const childProcess = require("child_process"); +const fs = require("fs"); +const os = require("os"); +const path = require("path"); +const { pathToFileURL } = require("url"); + +const repoRoot = path.resolve(__dirname, "../.."); +const benchmarkFile = path.join(__dirname, "objc-dispatch-benchmarks.js"); +const marker = "NS_BENCH_RESULT:"; +const defaultLegacyRepo = "/Users/dj/.codex/worktrees/0a0e/ios"; +const defaultMetadataPath = path.join( + repoRoot, + "build/derived-data/macos-tests/Build/Products/Debug/metadata-arm64.bin" +); +const defaultWorkRoot = path.join(repoRoot, "build/benchmarks/objc-dispatch"); + +function parseArgs(argv) { + const args = { + runtime: "all", + iterations: 250000, + warmupIterations: undefined, + includeNapiGsdOff: false, + includeLegacyAotOff: false, + legacyRepo: process.env.NS_LEGACY_IOS_REPO || defaultLegacyRepo, + metadataPath: process.env.METADATA_PATH || defaultMetadataPath, + destination: process.env.IOS_DESTINATION || "", + workRoot: defaultWorkRoot, + timeoutMs: 120000, + buildTimeoutMs: 15 * 60 * 1000, + napiPackageTgz: "", + napiVariantLabel: "", + skipBuild: false, + compareResults: "" + }; + + for (let i = 0; i < argv.length; i++) { + const arg = argv[i]; + const next = () => argv[++i]; + + if (arg === "--runtime") args.runtime = next(); + else if (arg.startsWith("--runtime=")) args.runtime = arg.slice("--runtime=".length); + else if (arg === "--iterations") args.iterations = Number(next()); + else if (arg.startsWith("--iterations=")) args.iterations = Number(arg.slice("--iterations=".length)); + else if (arg === "--warmup") args.warmupIterations = Number(next()); + else if (arg.startsWith("--warmup=")) args.warmupIterations = Number(arg.slice("--warmup=".length)); + else if (arg === "--legacy-repo") args.legacyRepo = path.resolve(next()); + else if (arg.startsWith("--legacy-repo=")) args.legacyRepo = path.resolve(arg.slice("--legacy-repo=".length)); + else if (arg === "--metadata-path") args.metadataPath = path.resolve(next()); + else if (arg.startsWith("--metadata-path=")) args.metadataPath = path.resolve(arg.slice("--metadata-path=".length)); + else if (arg === "--destination") args.destination = next(); + else if (arg.startsWith("--destination=")) args.destination = arg.slice("--destination=".length); + else if (arg === "--work-root") args.workRoot = path.resolve(next()); + else if (arg.startsWith("--work-root=")) args.workRoot = path.resolve(arg.slice("--work-root=".length)); + else if (arg === "--timeout-ms") args.timeoutMs = Number(next()); + else if (arg.startsWith("--timeout-ms=")) args.timeoutMs = Number(arg.slice("--timeout-ms=".length)); + else if (arg === "--build-timeout-ms") args.buildTimeoutMs = Number(next()); + else if (arg.startsWith("--build-timeout-ms=")) args.buildTimeoutMs = Number(arg.slice("--build-timeout-ms=".length)); + else if (arg === "--napi-package-tgz") args.napiPackageTgz = path.resolve(next()); + else if (arg.startsWith("--napi-package-tgz=")) args.napiPackageTgz = path.resolve(arg.slice("--napi-package-tgz=".length)); + else if (arg === "--napi-variant-label") args.napiVariantLabel = next(); + else if (arg.startsWith("--napi-variant-label=")) args.napiVariantLabel = arg.slice("--napi-variant-label=".length); + else if (arg === "--include-napi-gsd-off") args.includeNapiGsdOff = true; + else if (arg === "--include-legacy-aot-off") args.includeLegacyAotOff = true; + else if (arg === "--skip-build") args.skipBuild = true; + else if (arg === "--compare-results") args.compareResults = path.resolve(next()); + else if (arg.startsWith("--compare-results=")) args.compareResults = path.resolve(arg.slice("--compare-results=".length)); + else if (arg === "--help" || arg === "-h") { + printUsage(); + process.exit(0); + } else { + throw new Error(`Unknown argument: ${arg}`); + } + } + + if (!Number.isFinite(args.iterations) || args.iterations <= 0) { + throw new Error("--iterations must be a positive number"); + } + if (args.warmupIterations !== undefined && + (!Number.isFinite(args.warmupIterations) || args.warmupIterations < 0)) { + throw new Error("--warmup must be a non-negative number"); + } + + return args; +} + +function printUsage() { + console.log(`Usage: node benchmarks/objc-dispatch/run.js [options] + +Options: + --runtime all|napi-node|napi-ios|legacy-ios + --iterations N + --warmup N + --legacy-repo PATH Default: ${defaultLegacyRepo} + --metadata-path PATH Used by napi-node. Default: ${defaultMetadataPath} + --destination DEST_OR_UDID iOS simulator destination or UDID + --napi-package-tgz PATH @nativescript/ios package tgz for napi-ios + --napi-variant-label LABEL Prefix N-API iOS report variants with an engine/backend label + --include-napi-gsd-off Also run N-API with generated signature dispatch disabled + --include-legacy-aot-off Also run legacy iOS V8 with AOT disabled + --skip-build Reuse existing derived-data app builds + --compare-results PATH Print report and comparison tables from a saved result JSON +`); +} + +function run(command, args, options = {}) { + const result = childProcess.spawnSync(command, args, { + encoding: "utf8", + maxBuffer: 128 * 1024 * 1024, + ...options + }); + + if (result.error) { + throw result.error; + } + if (result.status !== 0) { + const details = [result.stdout, result.stderr].filter(Boolean).join("\n"); + throw new Error(`Command failed (${result.status}): ${command} ${args.join(" ")}\n${details}`); + } + return result; +} + +function runInherited(command, args, options = {}) { + const result = childProcess.spawnSync(command, args, { + stdio: "inherit", + ...options + }); + if (result.error) { + throw result.error; + } + if (result.status !== 0) { + throw new Error(`Command failed (${result.status}): ${command} ${args.join(" ")}`); + } +} + +function ensureDir(dir) { + fs.mkdirSync(dir, { recursive: true }); +} + +function rmrf(target) { + fs.rmSync(target, { recursive: true, force: true }); +} + +function copyDirectoryContents(sourceDir, destDir) { + rmrf(destDir); + ensureDir(destDir); + fs.cpSync(sourceDir, destDir, { recursive: true }); +} + +function writeJsonRunner(targetPath, runtime, variant, options) { + const payload = JSON.stringify({ + iterations: options.iterations, + warmupIterations: options.warmupIterations + }); + fs.writeFileSync( + targetPath, + [ + `global.__NS_BENCHMARK_RUNTIME = ${JSON.stringify(runtime)};`, + `global.__NS_BENCHMARK_VARIANT = ${JSON.stringify(variant)};`, + `global.__NS_BENCHMARK_OPTIONS__ = ${payload};`, + `require("./${path.basename(benchmarkFile)}");`, + "" + ].join("\n") + ); +} + +function parseBenchmarkOutput(output) { + let position = 0; + const results = []; + const skipped = []; + let done = null; + let sawMarker = false; + + while (position < output.length) { + const index = output.indexOf(marker, position); + if (index === -1) { + break; + } + sawMarker = true; + const parsed = parseJsonAfterMarker(output, index); + position = parsed.nextPosition; + if (!parsed.value) { + continue; + } + + const value = parsed.value; + if (value.kind === "case") { + results.push({ + name: value.name, + iterations: value.iterations, + ms: value.ms, + nsPerOp: value.nsPerOp + }); + } else if (value.kind === "skip") { + skipped.push({ name: value.name, error: value.error }); + } else if (value.kind === "done") { + done = value; + } else if (value.results) { + return value; + } + } + + if (done) { + return { + version: done.version, + runtime: done.runtime, + variant: done.variant, + baseIterations: done.baseIterations, + warmupIterations: done.warmupIterations, + totalMs: done.totalMs, + sink: done.sink, + results, + skipped + }; + } + + if (!sawMarker) { + throw new Error(`Benchmark marker not found in output:\n${output.slice(-4000)}`); + } + + throw new Error(`Benchmark done marker not found in output:\n${output.slice(-4000)}`); +} + +function parseJsonAfterMarker(output, index) { + const afterMarker = output.slice(index + marker.length); + const jsonStart = afterMarker.indexOf("{"); + if (jsonStart === -1) { + return { value: null, nextPosition: index + marker.length }; + } + + let depth = 0; + let inString = false; + let escaped = false; + for (let i = jsonStart; i < afterMarker.length; i++) { + const ch = afterMarker[i]; + if (inString) { + if (escaped) { + escaped = false; + } else if (ch === "\\") { + escaped = true; + } else if (ch === "\"") { + inString = false; + } + continue; + } + + if (ch === "\"") { + inString = true; + } else if (ch === "{") { + depth++; + } else if (ch === "}") { + depth--; + if (depth === 0) { + try { + return { + value: JSON.parse(afterMarker.slice(jsonStart, i + 1)), + nextPosition: index + marker.length + i + 1 + }; + } catch (_) { + return { value: null, nextPosition: index + marker.length + i + 1 }; + } + } + } + } + + return { value: null, nextPosition: index + marker.length }; +} + +function printReport(report) { + console.log(`\n${report.runtime} (${report.variant})`); + console.log("case".padEnd(40) + "ops".padStart(12) + "ms".padStart(12) + "ns/op".padStart(14)); + for (const result of report.results) { + console.log( + result.name.padEnd(40) + + String(result.iterations).padStart(12) + + result.ms.toFixed(2).padStart(12) + + result.nsPerOp.toFixed(1).padStart(14) + ); + } + if (report.skipped && report.skipped.length > 0) { + console.log("skipped: " + report.skipped.map((item) => item.name).join(", ")); + } +} + +function reportLabel(report) { + return `${report.runtime} (${report.variant})`; +} + +function labeledNapiVariant(options, variant) { + return options.napiVariantLabel ? `${options.napiVariantLabel} ${variant}` : variant; +} + +function napiVariantGroup(variant) { + const match = String(variant).match(/^(?:(.*)\s+)?(gsd-on|gsd-off)$/); + if (!match) { + return null; + } + return { + label: match[1] || "", + kind: match[2] + }; +} + +function resultMap(report) { + return new Map(report.results.map((result) => [result.name, result])); +} + +function formatSigned(value, digits = 2) { + const fixed = Math.abs(value).toFixed(digits); + if (value > 0) { + return `+${fixed}`; + } + if (value < 0) { + return `-${fixed}`; + } + return fixed; +} + +function formatPercent(value) { + return `${formatSigned(value, 1)}%`; +} + +function comparisonWinner(deltaNsPerOp) { + if (Math.abs(deltaNsPerOp) < 0.05) { + return "tie"; + } + return deltaNsPerOp < 0 ? "comparison" : "baseline"; +} + +function printTotalsComparison(reports, baseline) { + console.log(`\nTotal comparison, baseline ${reportLabel(baseline)}`); + console.log("| runtime | total ms | delta ms | ratio | relative |"); + console.log("|---|---:|---:|---:|---:|"); + for (const report of reports) { + const deltaMs = report.totalMs - baseline.totalMs; + const ratio = report.totalMs / baseline.totalMs; + const relative = (baseline.totalMs / report.totalMs) * 100; + console.log( + `| ${reportLabel(report)} | ${report.totalMs.toFixed(2)} | ${formatSigned(deltaMs)} | ${ratio.toFixed(2)}x | ${relative.toFixed(1)}% |` + ); + } +} + +function printPairComparison(baseline, comparison) { + const baselineResults = resultMap(baseline); + const comparisonResults = resultMap(comparison); + console.log(`\n${reportLabel(comparison)} vs ${reportLabel(baseline)}`); + console.log("| case | baseline ms | comparison ms | delta ms | baseline ns/op | comparison ns/op | delta ns/op | delta % | winner |"); + console.log("|---|---:|---:|---:|---:|---:|---:|---:|---|"); + for (const baselineResult of baseline.results) { + const comparisonResult = comparisonResults.get(baselineResult.name); + if (!comparisonResult) { + continue; + } + const deltaMs = comparisonResult.ms - baselineResult.ms; + const deltaNsPerOp = comparisonResult.nsPerOp - baselineResult.nsPerOp; + const deltaPercent = (deltaNsPerOp / baselineResult.nsPerOp) * 100; + console.log( + `| ${baselineResult.name} | ${baselineResult.ms.toFixed(2)} | ${comparisonResult.ms.toFixed(2)} | ${formatSigned(deltaMs)} | ${baselineResult.nsPerOp.toFixed(1)} | ${comparisonResult.nsPerOp.toFixed(1)} | ${formatSigned(deltaNsPerOp, 1)} | ${formatPercent(deltaPercent)} | ${comparisonWinner(deltaNsPerOp)} |` + ); + } +} + +function printComparisons(reports) { + if (!Array.isArray(reports) || reports.length < 2) { + return; + } + + const baseline = reports[0]; + printTotalsComparison(reports, baseline); + + const napiGroups = new Map(); + for (const report of reports) { + if (report.runtime !== "napi-ios") { + continue; + } + const group = napiVariantGroup(report.variant); + if (!group) { + continue; + } + const key = group.label; + if (!napiGroups.has(key)) { + napiGroups.set(key, new Map()); + } + napiGroups.get(key).set(group.kind, report); + } + + let napiGsdOn = null; + for (const group of napiGroups.values()) { + const gsdOn = group.get("gsd-on"); + const gsdOff = group.get("gsd-off"); + if (gsdOn && !napiGsdOn) { + napiGsdOn = gsdOn; + } + if (gsdOn && gsdOff) { + printPairComparison(gsdOn, gsdOff); + } + } + + const legacyAotOn = reports.find((report) => report.runtime === "legacy-ios" && report.variant === "aot-on"); + if (napiGsdOn && legacyAotOn) { + printPairComparison(napiGsdOn, legacyAotOn); + } + + const legacyAotOff = reports.find((report) => report.runtime === "legacy-ios" && report.variant === "aot-off"); + if (napiGsdOn && legacyAotOff) { + printPairComparison(napiGsdOn, legacyAotOff); + } +} + +function printSavedResultsComparison(resultsPath) { + const parsed = JSON.parse(fs.readFileSync(resultsPath, "utf8")); + const reports = parsed.reports || []; + for (const report of reports) { + printReport(report); + } + printComparisons(reports); +} + +function runNapiNode(options, variant) { + if (!fs.existsSync(options.metadataPath)) { + throw new Error(`Metadata file not found: ${options.metadataPath}`); + } + + const runnerDir = path.join(options.workRoot, "node"); + ensureDir(runnerDir); + fs.copyFileSync(benchmarkFile, path.join(runnerDir, path.basename(benchmarkFile))); + + const runnerPath = path.join(runnerDir, `run-${variant}.cjs`); + const benchmarkOptions = { + iterations: options.iterations, + warmupIterations: options.warmupIterations + }; + fs.writeFileSync( + runnerPath, + [ + `global.__NS_BENCHMARK_RUNTIME = "napi-node";`, + `global.__NS_BENCHMARK_VARIANT = ${JSON.stringify(variant)};`, + `global.__NS_BENCHMARK_OPTIONS__ = ${JSON.stringify(benchmarkOptions)};`, + `import(${JSON.stringify(pathToFileUrl(path.join(repoRoot, "packages/macos-node-api/index.mjs")))}).then(() => {`, + ` require(${JSON.stringify(path.join(runnerDir, path.basename(benchmarkFile)))});`, + `}).catch((error) => { console.error(error && error.stack || error); process.exit(1); });`, + "" + ].join("\n") + ); + + const env = { ...process.env, METADATA_PATH: options.metadataPath }; + if (variant === "gsd-off") { + // Current runtime disables generated signature dispatch when this value is exactly "0". + env.NS_DISABLE_GSD = "0"; + } else { + delete env.NS_DISABLE_GSD; + } + + const result = run(process.execPath, [runnerPath], { cwd: repoRoot, env, timeout: options.timeoutMs }); + return parseBenchmarkOutput(result.stdout + result.stderr); +} + +function pathToFileUrl(filePath) { + return pathToFileURL(filePath).href; +} + +function destinationToUdid(destination) { + if (!destination) { + return ""; + } + const idMatch = destination.match(/id=([0-9A-Fa-f-]{36})/); + if (idMatch) { + return idMatch[1]; + } + if (/^[0-9A-Fa-f-]{36}$/.test(destination)) { + return destination; + } + return ""; +} + +function pickSimulator(destination) { + const explicit = destinationToUdid(destination); + if (explicit) { + return explicit; + } + + const result = run("xcrun", ["simctl", "list", "devices", "available", "--json"]); + const parsed = JSON.parse(result.stdout); + const devices = []; + for (const runtimeName of Object.keys(parsed.devices || {})) { + for (const device of parsed.devices[runtimeName]) { + if (device.isAvailable && /iPhone/.test(device.name)) { + devices.push(device); + } + } + } + + const booted = devices.find((device) => device.state === "Booted"); + const preferred = booted || devices.find((device) => /Pro/.test(device.name)) || devices[0]; + if (!preferred) { + throw new Error("No available iPhone simulator found"); + } + return preferred.udid; +} + +function bootSimulator(udid) { + const boot = childProcess.spawnSync("xcrun", ["simctl", "boot", udid], { encoding: "utf8" }); + if (boot.status !== 0 && !/Unable to boot device in current state: Booted/.test(boot.stderr || "")) { + throw new Error(`Unable to boot simulator ${udid}:\n${boot.stderr || boot.stdout}`); + } + runInherited("xcrun", ["simctl", "bootstatus", udid, "-b"], { timeout: 180000 }); +} + +function findBuiltApp(derivedDataPath, appName) { + const productsRoot = path.join(derivedDataPath, "Build/Products"); + const queue = [productsRoot]; + while (queue.length > 0) { + const current = queue.pop(); + if (!fs.existsSync(current)) { + continue; + } + const stats = fs.statSync(current); + if (stats.isDirectory() && path.basename(current) === `${appName}.app`) { + return current; + } + if (stats.isDirectory()) { + for (const entry of fs.readdirSync(current)) { + queue.push(path.join(current, entry)); + } + } + } + throw new Error(`Built app not found under ${productsRoot}`); +} + +function launchAndCollect(udid, bundleId, options, env = {}) { + return new Promise((resolve, reject) => { + let output = ""; + let settled = false; + const children = []; + const launchEnv = { ...process.env }; + for (const [key, value] of Object.entries(env)) { + launchEnv[`SIMCTL_CHILD_${key}`] = value; + } + + const logChild = childProcess.spawn( + "xcrun", + [ + "simctl", "spawn", udid, + "log", "stream", + "--style", "compact", + "--level", "debug", + "--predicate", `eventMessage CONTAINS "${marker}"` + ], + { env: process.env } + ); + children.push(logChild); + + const child = childProcess.spawn( + "xcrun", + ["simctl", "launch", "--terminate-running-process", udid, bundleId], + { env: launchEnv } + ); + children.push(child); + + const timeout = setTimeout(() => { + if (settled) { + return; + } + settled = true; + for (const activeChild of children) { + activeChild.kill("SIGTERM"); + } + childProcess.spawnSync("xcrun", ["simctl", "terminate", udid, bundleId], { stdio: "ignore" }); + reject(new Error(`Timed out waiting for benchmark marker from ${bundleId}`)); + }, options.timeoutMs); + + function settleWithReport(report) { + if (settled) { + return; + } + settled = true; + clearTimeout(timeout); + for (const activeChild of children) { + activeChild.kill("SIGTERM"); + } + childProcess.spawnSync("xcrun", ["simctl", "terminate", udid, bundleId], { stdio: "ignore" }); + resolve(report); + } + + function onData(data) { + const text = data.toString(); + output += text; + process.stdout.write(text); + if (!settled && output.includes(marker)) { + try { + settleWithReport(parseBenchmarkOutput(output)); + } catch (_) { + // log stream prints the predicate itself before app logs; wait for the + // actual console message containing marker JSON. + } + } + } + + logChild.stdout.on("data", onData); + logChild.stderr.on("data", onData); + child.stdout.on("data", onData); + child.stderr.on("data", onData); + for (const activeChild of children) { + activeChild.on("error", (error) => { + if (!settled) { + settled = true; + clearTimeout(timeout); + reject(error); + } + }); + } + child.on("exit", (code, signal) => { + if (!settled) { + if (output.includes(marker)) { + try { + settleWithReport(parseBenchmarkOutput(output)); + return; + } catch (_) { + // The unified log stream may still deliver the actual message after + // simctl launch exits. + } + } + if (code !== 0 || signal) { + settled = true; + clearTimeout(timeout); + for (const activeChild of children) { + activeChild.kill("SIGTERM"); + } + childProcess.spawnSync("xcrun", ["simctl", "terminate", udid, bundleId], { stdio: "ignore" }); + reject(new Error( + `simctl launch for ${bundleId} exited with code ${code ?? "unknown"}${signal ? ` (signal ${signal})` : ""} before emitting ${marker}.\n${output}` + )); + } + } + }); + }); +} + +function xcodebuild(args, cwd, timeoutMs) { + runInherited("xcodebuild", args, { + cwd, + timeout: timeoutMs, + env: { + ...process.env, + PATH: ["/opt/homebrew/bin", "/usr/local/bin", process.env.PATH || ""].join(":"), + NSUnbufferedIO: "YES" + } + }); +} + +function installApp(udid, appPath, bundleId) { + childProcess.spawnSync("xcrun", ["simctl", "terminate", udid, bundleId], { stdio: "ignore" }); + childProcess.spawnSync("xcrun", ["simctl", "uninstall", udid, bundleId], { stdio: "ignore" }); + runInherited("xcrun", ["simctl", "install", udid, appPath], { timeout: 120000 }); +} + +function readAppBundleId(appPath, fallback) { + const plistPath = path.join(appPath, "Info.plist"); + if (!fs.existsSync(plistPath)) { + return fallback; + } + const result = childProcess.spawnSync( + "/usr/libexec/PlistBuddy", + ["-c", "Print :CFBundleIdentifier", plistPath], + { encoding: "utf8" } + ); + if (result.status === 0 && result.stdout.trim()) { + return result.stdout.trim(); + } + return fallback; +} + +async function runLegacyIOS(options, variant = "aot-on") { + const appName = "TestRunner"; + let bundleId = "com.descendra.TestRunner"; + const appDir = path.join(options.legacyRepo, "TestRunner/app"); + const indexPath = path.join(appDir, "index.js"); + const copiedBenchmarkPath = path.join(appDir, path.basename(benchmarkFile)); + const originalIndex = fs.readFileSync(indexPath, "utf8"); + const derivedDataPath = path.join(options.workRoot, "derived-data/legacy-ios"); + const udid = pickSimulator(options.destination); + + ensureDir(path.dirname(copiedBenchmarkPath)); + fs.copyFileSync(benchmarkFile, copiedBenchmarkPath); + writeJsonRunner(indexPath, "legacy-ios", variant, options); + + try { + bootSimulator(udid); + if (!options.skipBuild) { + xcodebuild([ + "-project", "v8ios.xcodeproj", + "-scheme", "TestRunner", + "-configuration", "Release", + "-destination", `platform=iOS Simulator,id=${udid}`, + "-derivedDataPath", derivedDataPath, + "CODE_SIGNING_ALLOWED=NO", + "CODE_SIGNING_REQUIRED=NO", + "CLANG_WARN_NULLABLE_TO_NONNULL_CONVERSION=NO", + "CLANG_WARN_NULLABILITY_COMPLETENESS=NO", + "OTHER_CPLUSPLUSFLAGS=-fno-rtti -Wall -Werror -Wno-documentation -Wno-deprecated-declarations -Wno-nullability-completeness -Wno-unknown-pragmas -Wno-unreachable-code -Wno-strict-prototypes -fembed-bitcode", + "OTHER_CFLAGS=-fno-rtti -Wall -Werror -Wno-documentation -Wno-deprecated-declarations -Wno-nullability-completeness -Wno-unknown-pragmas -Wno-unreachable-code -Wno-strict-prototypes -fembed-bitcode", + "build", + "-quiet" + ], options.legacyRepo, options.buildTimeoutMs); + } + let appPath; + try { + appPath = findBuiltApp(derivedDataPath, appName); + } catch (_) { + appPath = path.join(options.legacyRepo, "build/Release-iphonesimulator", `${appName}.app`); + if (!fs.existsSync(appPath)) { + throw _; + } + } + if (options.skipBuild) { + copyDirectoryContents(appDir, path.join(appPath, "app")); + } + bundleId = readAppBundleId(appPath, bundleId); + installApp(udid, appPath, bundleId); + const launchEnv = variant === "aot-off" ? { NS_DISABLE_AOT: "1" } : {}; + return await launchAndCollect(udid, bundleId, options, launchEnv); + } finally { + fs.writeFileSync(indexPath, originalIndex); + rmrf(copiedBenchmarkPath); + } +} + +function findDefaultNapiPackage() { + const distDir = path.join(repoRoot, "packages/ios/dist"); + const names = fs.readdirSync(distDir) + .filter((name) => /^nativescript-ios-.*\.tgz$/.test(name)) + .sort(); + if (names.length === 0) { + throw new Error(`No @nativescript/ios package tgz found in ${distDir}`); + } + return path.join(distDir, names[names.length - 1]); +} + +function replaceInTextFiles(root, search, replacement) { + const queue = [root]; + while (queue.length > 0) { + const current = queue.pop(); + const stats = fs.lstatSync(current); + if (stats.isDirectory()) { + for (const entry of fs.readdirSync(current)) { + queue.push(path.join(current, entry)); + } + continue; + } + if (!stats.isFile()) { + continue; + } + const buffer = fs.readFileSync(current); + if (buffer.includes(0)) { + continue; + } + const text = buffer.toString("utf8"); + if (text.includes(search)) { + fs.writeFileSync(current, text.split(search).join(replacement)); + } + } +} + +function renamePlaceholderPaths(root, search, replacement) { + const entries = []; + const queue = [root]; + while (queue.length > 0) { + const current = queue.pop(); + entries.push(current); + if (fs.lstatSync(current).isDirectory()) { + for (const entry of fs.readdirSync(current)) { + queue.push(path.join(current, entry)); + } + } + } + + entries.sort((a, b) => b.length - a.length); + for (const current of entries) { + const base = path.basename(current); + if (!base.includes(search) || !fs.existsSync(current)) { + continue; + } + fs.renameSync(current, path.join(path.dirname(current), base.split(search).join(replacement))); + } +} + +function scaffoldNapiIOSApp(options, variant, packageTgz, reportVariant = variant) { + const appName = "NativeScriptDispatchBench"; + const bundleId = "org.nativescript.bench.dispatch.napi"; + const tgz = packageTgz || options.napiPackageTgz || findDefaultNapiPackage(); + const root = path.join(options.workRoot, "apps", `napi-ios-${variant}`); + rmrf(root); + ensureDir(root); + run("tar", ["-xzf", tgz, "-C", root]); + + const frameworkRoot = path.join(root, "package/framework"); + const projectPath = path.join(frameworkRoot, `${appName}.xcodeproj`); + const appSourceRoot = path.join(frameworkRoot, appName); + + fs.renameSync( + path.join(frameworkRoot, "__PROJECT_NAME__.xcodeproj"), + projectPath + ); + fs.renameSync( + path.join(frameworkRoot, "__PROJECT_NAME__"), + appSourceRoot + ); + + for (const name of fs.readdirSync(appSourceRoot)) { + if (name.includes("__PROJECT_NAME__")) { + fs.renameSync( + path.join(appSourceRoot, name), + path.join(appSourceRoot, name.replaceAll("__PROJECT_NAME__", appName)) + ); + } + } + + renamePlaceholderPaths(frameworkRoot, "__PROJECT_NAME__", appName); + replaceInTextFiles(frameworkRoot, "__PROJECT_NAME__", appName); + replaceInTextFiles(frameworkRoot, "config.LogToSystemConsole = isDebug;", "config.LogToSystemConsole = YES;"); + fs.writeFileSync(path.join(frameworkRoot, "plugins-debug.xcconfig"), "\n"); + fs.writeFileSync(path.join(frameworkRoot, "plugins-release.xcconfig"), "\n"); + writeInfoPlist(path.join(appSourceRoot, `${appName}-Info.plist`)); + + const appDir = path.join(appSourceRoot, "app"); + ensureDir(appDir); + fs.writeFileSync(path.join(appDir, "package.json"), JSON.stringify({ main: "index" }, null, 2) + "\n"); + fs.copyFileSync(benchmarkFile, path.join(appDir, path.basename(benchmarkFile))); + writeJsonRunner(path.join(appDir, "index.js"), "napi-ios", reportVariant, options); + + const zipPath = path.join(frameworkRoot, "internal/XCFrameworks.zip"); + run("unzip", ["-q", "-o", zipPath, "-d", path.join(frameworkRoot, "internal")]); + + return { appName, bundleId, frameworkRoot, projectPath, appDir }; +} + +function writeInfoPlist(plistPath) { + fs.writeFileSync(plistPath, ` + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + + +`); +} + +async function runNapiIOS(options, variant, packageTgz, reportVariant = variant) { + const app = scaffoldNapiIOSApp(options, variant, packageTgz, reportVariant); + const derivedDataPath = path.join(options.workRoot, `derived-data/napi-ios-${variant}`); + const udid = pickSimulator(options.destination); + + bootSimulator(udid); + if (!options.skipBuild) { + xcodebuild([ + "-project", app.projectPath, + "-scheme", app.appName, + "-configuration", "Release", + "-destination", `platform=iOS Simulator,id=${udid}`, + "-derivedDataPath", derivedDataPath, + "CODE_SIGNING_ALLOWED=NO", + "CODE_SIGNING_REQUIRED=NO", + `PRODUCT_BUNDLE_IDENTIFIER=${app.bundleId}`, + "ARCHS=arm64", + "ONLY_ACTIVE_ARCH=YES", + "EXCLUDED_ARCHS=", + "build", + "-quiet" + ], app.frameworkRoot, options.buildTimeoutMs); + } + + const appPath = findBuiltApp(derivedDataPath, app.appName); + if (options.skipBuild) { + copyDirectoryContents(app.appDir, path.join(appPath, "app")); + } + installApp(udid, appPath, app.bundleId); + const launchEnv = variant === "gsd-off" ? { NS_DISABLE_GSD: "0" } : {}; + return await launchAndCollect(udid, app.bundleId, options, launchEnv); +} + +async function main() { + const options = parseArgs(process.argv.slice(2)); + if (options.compareResults) { + printSavedResultsComparison(options.compareResults); + return; + } + + ensureDir(options.workRoot); + + const reports = []; + const runtimes = options.runtime === "all" + ? ["napi-node", "napi-ios", "legacy-ios"] + : options.runtime.split(",").map((item) => item.trim()).filter(Boolean); + + for (const runtime of runtimes) { + if (runtime === "napi-node") { + reports.push(runNapiNode(options, "gsd-on")); + if (options.includeNapiGsdOff) { + reports.push(runNapiNode(options, "gsd-off")); + } + } else if (runtime === "napi-ios") { + reports.push(await runNapiIOS(options, "gsd-on", undefined, labeledNapiVariant(options, "gsd-on"))); + if (options.includeNapiGsdOff) { + reports.push(await runNapiIOS(options, "gsd-off", undefined, labeledNapiVariant(options, "gsd-off"))); + } + } else if (runtime === "legacy-ios") { + reports.push(await runLegacyIOS(options, "aot-on")); + if (options.includeLegacyAotOff) { + reports.push(await runLegacyIOS({ ...options, skipBuild: true }, "aot-off")); + } + } else { + throw new Error(`Unknown runtime: ${runtime}`); + } + } + + for (const report of reports) { + printReport(report); + } + printComparisons(reports); + + const outPath = path.join(options.workRoot, `results-${new Date().toISOString().replace(/[:.]/g, "-")}.json`); + fs.writeFileSync(outPath, JSON.stringify({ createdAt: new Date().toISOString(), reports }, null, 2) + "\n"); + console.log(`\nWrote ${outPath}`); +} + +main().catch((error) => { + console.error(error && error.stack ? error.stack : error); + process.exit(1); +}); diff --git a/examples/expo-demo/App.tsx b/examples/expo-demo/App.tsx new file mode 100644 index 00000000..657b6aac --- /dev/null +++ b/examples/expo-demo/App.tsx @@ -0,0 +1,122 @@ +import React, {useEffect, useMemo, useState} from 'react'; +import {Pressable, ScrollView, Text, View} from 'react-native'; +import NativeScript, {defineUIKitView} from '@nativescript/react-native'; + +NativeScript.init(); + +type BadgeProps = { + title: string; + tone: 'blue' | 'green'; +}; + +const NativeBadge = defineUIKitView({ + displayName: 'NativeBadge', + create() { + const view = UIView.alloc().initWithFrame(CGRectZero); + view.clipsToBounds = true; + + const label = UILabel.alloc().initWithFrame(CGRectZero); + label.tag = 1; + label.textAlignment = NSTextAlignment.Center; + label.textColor = UIColor.whiteColor; + label.autoresizingMask = + UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight; + view.addSubview(label); + + return view; + }, + mounted(view) { + view.layer.cornerRadius = 14; + }, + update(view, props) { + view.backgroundColor = + props.tone === 'green' ? UIColor.systemGreenColor : UIColor.systemBlueColor; + const label = view.viewWithTag(1) as UILabel; + label.text = props.title; + }, +}); + +async function readNativeSummary() { + const api = (globalThis as any).__nativeScriptNativeApi; + let ranOnMainThread = false; + + await NativeScript.runOnUI(() => { + ranOnMainThread = NSThread.isMainThread === true; + UIApplication.sharedApplication.keyWindow.tintColor = + UIColor.systemPinkColor; + }); + + return { + backend: api?.backend, + classes: api?.metadata?.classes ?? 0, + constants: api?.metadata?.constants ?? 0, + enums: api?.metadata?.enums ?? 0, + ranOnMainThread, + timeoutConstant: NSURLErrorTimedOut, + darkStyle: UIUserInterfaceStyle.Dark, + }; +} +export default function App() { + const [tone, setTone] = useState<'blue' | 'green'>('blue'); + const [summary, setSummary] = useState('Loading NativeScript...'); + + useEffect(() => { + readNativeSummary() + .then((value) => setSummary(JSON.stringify(value, null, 2))) + .catch((error) => { + setSummary(error instanceof Error ? error.message : String(error)); + }); + }, []); + + const title = useMemo( + () => (tone === 'blue' ? 'UIKit view from Expo' : 'Updated from React state'), + [tone], + ); + + return ( + + + + NativeScript Expo + + + Define UIKit views directly in JavaScript and mount them in an Expo + development build. + + + + + + setTone((value) => (value === 'blue' ? 'green' : 'blue'))} + style={{ + alignItems: 'center', + borderRadius: 8, + backgroundColor: '#111827', + minHeight: 48, + justifyContent: 'center', + paddingHorizontal: 16, + }}> + + Toggle Native Badge + + + + + {summary} + + + ); +} diff --git a/examples/expo-demo/README.md b/examples/expo-demo/README.md new file mode 100644 index 00000000..5ebc12c6 --- /dev/null +++ b/examples/expo-demo/README.md @@ -0,0 +1,17 @@ +# NativeScript Expo Demo + +This example is meant to be copied into a generated Expo app after installing +`@nativescript/react-native`. + +```sh +npx create-expo-app NativeScriptExpoDemo --template blank-typescript +cd NativeScriptExpoDemo +npm install /path/to/nativescript-react-native-0.0.1.tgz +cp /path/to/napi-ios/examples/expo-demo/app.config.js ./app.config.js +cp /path/to/napi-ios/examples/expo-demo/App.tsx ./App.tsx +npx expo prebuild --platform ios +npx expo run:ios +``` + +The package config plugin enables the iOS New Architecture and Hermes during +prebuild. This custom native module cannot run inside Expo Go. diff --git a/examples/expo-demo/app.config.js b/examples/expo-demo/app.config.js new file mode 100644 index 00000000..70135997 --- /dev/null +++ b/examples/expo-demo/app.config.js @@ -0,0 +1,11 @@ +module.exports = { + expo: { + name: 'NativeScript Expo Demo', + slug: 'nativescript-expo-demo', + scheme: 'nativescriptexpodemo', + ios: { + bundleIdentifier: 'org.nativescript.expo.demo', + }, + plugins: ['@nativescript/react-native'], + }, +}; diff --git a/examples/react-native-demo/App.tsx b/examples/react-native-demo/App.tsx new file mode 100644 index 00000000..f03cf6b4 --- /dev/null +++ b/examples/react-native-demo/App.tsx @@ -0,0 +1,226 @@ +import React, {useCallback, useEffect, useMemo, useState} from 'react'; +import { + Platform, + Pressable, + SafeAreaView, + StyleSheet, + Text, + View, +} from 'react-native'; +import NativeScript from '@nativescript/react-native'; + +type NativeApiHost = { + backend?: string; + metadata?: { + classes?: number; + functions?: number; + constants?: number; + enums?: number; + }; +}; + +function installNativeScriptGlobals(): NativeApiHost { + NativeScript.init(); + const api = (globalThis as any).__nativeScriptNativeApi as + | NativeApiHost + | undefined; + if (!api) { + throw new Error('NativeScript Native API JSI host object was not installed'); + } + return api; +} + +function getActiveUIKitWindow() { + const app = UIApplication.sharedApplication; + const scenes = app.connectedScenes?.allObjects; + if (scenes) { + for (let i = 0; i < scenes.count; i++) { + const scene = scenes.objectAtIndex(i); + if ( + scene instanceof UIWindowScene && + scene.activationState === UISceneActivationState.ForegroundActive + ) { + const windows = scene.windows; + for (let j = 0; j < windows.count; j++) { + const window = windows.objectAtIndex(j); + if (window.isKeyWindow) { + return window; + } + } + if (windows.count > 0) { + return windows.objectAtIndex(0); + } + } + } + } + return app.keyWindow; +} + +async function applyUIKitTweaks() { + if (Platform.OS !== 'ios') { + throw new Error('This demo uses UIKit and must run on iOS'); + } + + const api = installNativeScriptGlobals(); + + let nativeCallsRanOnMainThread = false; + await NativeScript.runOnUI(() => { + nativeCallsRanOnMainThread = NSThread.isMainThread === true; + if (!nativeCallsRanOnMainThread) { + throw new Error('runOnUI did not dispatch native calls to the main thread'); + } + + const window = getActiveUIKitWindow(); + if (!window) { + throw new Error('No key UIWindow is available yet'); + } + + const nativeAccent = UIColor.systemPinkColor ?? UIColor.magentaColor; + const nativeBackdrop = UIColor.colorWithRedGreenBlueAlpha( + 0.04, + 0.08, + 0.12, + 1, + ); + + window.tintColor = nativeAccent; + window.backgroundColor = nativeBackdrop; + window.overrideUserInterfaceStyle = UIUserInterfaceStyle.Dark; + + const rootView = window.rootViewController?.view; + if (rootView) { + rootView.tintColor = nativeAccent; + rootView.backgroundColor = nativeBackdrop; + } + }); + + return { + backend: api.backend, + turboBackend: NativeScript.getRuntimeBackend(), + classes: api.metadata?.classes ?? 0, + functions: api.metadata?.functions ?? 0, + constants: api.metadata?.constants ?? 0, + enums: api.metadata?.enums ?? 0, + timeoutConstant: NSURLErrorTimedOut, + darkStyle: UIUserInterfaceStyle.Dark, + nativeCallsRanOnMainThread, + }; +} + +export default function App(): React.JSX.Element { + const [status, setStatus] = useState('Ready'); + const [details, setDetails] = useState(''); + const [busy, setBusy] = useState(false); + + const runDemo = useCallback(async () => { + setBusy(true); + setStatus('Applying UIKit tweaks'); + try { + const result = await applyUIKitTweaks(); + setStatus('UIKit updated from JavaScript'); + setDetails(JSON.stringify(result, null, 2)); + } catch (error) { + setStatus('Demo failed'); + setDetails(error instanceof Error ? error.message : String(error)); + } finally { + setBusy(false); + } + }, []); + + useEffect(() => { + runDemo(); + }, [runDemo]); + + const buttonLabel = useMemo( + () => (busy ? 'Working...' : 'Run UIKit Tweak Again'), + [busy], + ); + + return ( + + + NativeScript for React Native + Hermes JSI UIKit Demo + {status} + + {details} + + [ + styles.button, + busy && styles.buttonDisabled, + pressed && !busy && styles.buttonPressed, + ]}> + {buttonLabel} + + + + ); +} + +const styles = StyleSheet.create({ + screen: { + flex: 1, + backgroundColor: 'transparent', + justifyContent: 'center', + padding: 24, + }, + panel: { + gap: 14, + borderRadius: 8, + borderWidth: StyleSheet.hairlineWidth, + borderColor: 'rgba(255,255,255,0.26)', + backgroundColor: 'rgba(8,14,22,0.82)', + padding: 20, + }, + kicker: { + color: '#9bd4ff', + fontSize: 13, + fontWeight: '700', + letterSpacing: 0, + textTransform: 'uppercase', + }, + title: { + color: '#ffffff', + fontSize: 28, + fontWeight: '800', + letterSpacing: 0, + }, + status: { + color: '#f7d276', + fontSize: 17, + fontWeight: '700', + letterSpacing: 0, + }, + details: { + minHeight: 112, + color: '#d8e7f4', + fontFamily: Platform.select({ios: 'Menlo', default: 'monospace'}), + fontSize: 13, + lineHeight: 18, + letterSpacing: 0, + }, + button: { + minHeight: 46, + alignItems: 'center', + justifyContent: 'center', + borderRadius: 8, + backgroundColor: '#ff4fa3', + paddingHorizontal: 16, + }, + buttonPressed: { + backgroundColor: '#e23f90', + }, + buttonDisabled: { + opacity: 0.6, + }, + buttonText: { + color: '#ffffff', + fontSize: 16, + fontWeight: '800', + letterSpacing: 0, + }, +}); diff --git a/examples/react-native-demo/README.md b/examples/react-native-demo/README.md new file mode 100644 index 00000000..651f18a0 --- /dev/null +++ b/examples/react-native-demo/README.md @@ -0,0 +1,19 @@ +# React Native NativeScript Demo + +This demo is generated into `build/react-native-demo` so the +repository does not need to commit React Native boilerplate. + +Run it from the repository root: + +```sh +npm run demo-rn-turbomodule +``` + +The generated app installs the local +`@nativescript/react-native` tarball, enables Hermes and the New +Architecture, then launches an iOS simulator app. The app installs the +NativeScript Native API JSI host object with `NativeScript.init()`, installs +NativeScript-style globals such as `UIApplication` and `UIColor`, and uses +`runOnUI` to execute a small UIKit tweak from JavaScript while dispatching the +native UIKit calls to the main thread. The script waits for a simulator marker +after the tweak succeeds. diff --git a/metadata-generator/CMakeLists.txt b/metadata-generator/CMakeLists.txt index a6aea0d4..ada4ad98 100644 --- a/metadata-generator/CMakeLists.txt +++ b/metadata-generator/CMakeLists.txt @@ -17,11 +17,17 @@ endif(NOT DEFINED METADATA_BINARY_ARCH) include_directories( ${CMAKE_CURRENT_SOURCE_DIR}/include + ${CMAKE_CURRENT_SOURCE_DIR}/src ) set(EXEC_SOURCE_FILES src/main.cpp src/SignatureDispatchEmitter.cpp + src/SignatureDispatchEmitter/EngineDirect.cpp + src/SignatureDispatchEmitter/Hermes.cpp + src/SignatureDispatchEmitter/Napi.cpp + src/SignatureDispatchEmitter/Shared.cpp + src/SignatureDispatchEmitter/V8.cpp src/Umbrella.cpp src/IR/Category.cpp src/IR/Class.cpp diff --git a/metadata-generator/build-step-metadata-generator.py b/metadata-generator/build-step-metadata-generator.py index 4939377b..f00ead7f 100755 --- a/metadata-generator/build-step-metadata-generator.py +++ b/metadata-generator/build-step-metadata-generator.py @@ -172,7 +172,7 @@ def is_nativescript_source_root(search_path): strict_includes = env_or_none("NS_DEBUG_METADATA_STRICT_INCLUDES") or env_or_none("TNS_DEBUG_METADATA_STRICT_INCLUDES") signature_bindings_cpp_path = env_or_none("NS_SIGNATURE_BINDINGS_CPP_PATH") or env_or_none("TNS_SIGNATURE_BINDINGS_CPP_PATH") if signature_bindings_cpp_path is None: - default_signature_bindings_path = os.path.join(src_root, "NativeScript", "ffi", "GeneratedSignatureDispatch.inc") + default_signature_bindings_path = os.path.join(src_root, "NativeScript", "ffi", "napi", "GeneratedSignatureDispatch.inc") if os.path.isdir(os.path.dirname(default_signature_bindings_path)): signature_bindings_cpp_path = default_signature_bindings_path diff --git a/metadata-generator/src/SignatureDispatchEmitter.cpp b/metadata-generator/src/SignatureDispatchEmitter.cpp index 94069b62..da50820e 100644 --- a/metadata-generator/src/SignatureDispatchEmitter.cpp +++ b/metadata-generator/src/SignatureDispatchEmitter.cpp @@ -1,848 +1,17 @@ #include "SignatureDispatchEmitter.h" +#include "SignatureDispatchEmitter/Shared.h" #include -#include #include -#include #include #include -#include #include #include #include namespace metagen { -namespace { - -enum class DispatchKind : uint8_t { - ObjCMethod = 1, - CFunction = 2, -}; - -struct SignatureUse { - DispatchKind kind; - MDSectionOffset signatureOffset; - uint8_t flags; -}; - -constexpr uint64_t kFNV64OffsetBasis = 14695981039346656037ull; -constexpr uint64_t kFNV64Prime = 1099511628211ull; - -uint64_t hashBytesFnv1a(const void* data, size_t size, - uint64_t seed = kFNV64OffsetBasis) { - const auto* bytes = static_cast(data); - uint64_t hash = seed; - for (size_t i = 0; i < size; i++) { - hash ^= static_cast(bytes[i]); - hash *= kFNV64Prime; - } - return hash; -} - -uint64_t composeDispatchId(uint64_t signatureHash, DispatchKind 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); -} - -using SignatureMap = std::unordered_map; - -MDTypeKind canonicalizeSignatureTypeKind(MDTypeKind kind) { - switch (kind) { - case mdTypeAnyObject: - case mdTypeProtocolObject: - case mdTypeClassObject: - case mdTypeInstanceObject: - case mdTypeNSStringObject: - case mdTypeNSMutableStringObject: - return mdTypeAnyObject; - default: - return kind; - } -} - -template -void appendIntegral(uint64_t* hash, std::string* key, T value) { - using Unsigned = typename std::make_unsigned::type; - Unsigned unsignedValue = static_cast(value); - for (size_t i = 0; i < sizeof(Unsigned); i++) { - const uint8_t byte = - static_cast((unsignedValue >> (i * 8)) & 0xFF); - *hash = hashBytesFnv1a(&byte, sizeof(byte), *hash); - if (key != nullptr) { - key->push_back(static_cast(byte)); - } - } -} - -bool appendCanonicalSignature( - const MDSignature* signature, MDSectionOffset signatureOffset, - const SignatureMap& signatures, - std::unordered_set* activeSignatures, uint64_t* hash, - std::string* key); - -bool appendCanonicalType(const MDTypeInfo* type, const SignatureMap& signatures, - std::unordered_set* activeSignatures, - uint64_t* hash, std::string* key) { - if (type == nullptr || hash == nullptr) { - return false; - } - - appendIntegral(hash, key, 0xB0); - const MDTypeKind rawKind = type->kind; - const MDTypeKind canonicalKind = canonicalizeSignatureTypeKind(rawKind); - appendIntegral(hash, key, static_cast(canonicalKind)); - - switch (rawKind) { - case mdTypeArray: - case mdTypeVector: - case mdTypeExtVector: - case mdTypeComplex: - appendIntegral(hash, key, type->arraySize); - if (!appendCanonicalType(type->elementType, signatures, activeSignatures, - hash, key)) { - return false; - } - break; - - case mdTypeStruct: - appendIntegral(hash, key, type->structOffset); - break; - - case mdTypePointer: - if (!appendCanonicalType(type->pointeeType, signatures, activeSignatures, - hash, key)) { - return false; - } - break; - - case mdTypeBlock: - case mdTypeFunctionPointer: { - const MDSectionOffset nestedSignatureOffset = type->signatureOffset; - auto nestedIt = signatures.find(nestedSignatureOffset); - if (nestedSignatureOffset == MD_SECTION_OFFSET_NULL || - nestedIt == signatures.end() || nestedIt->second == nullptr) { - break; - } - - if (!appendCanonicalSignature(nestedIt->second, nestedSignatureOffset, - signatures, activeSignatures, hash, key)) { - return false; - } - break; - } - - default: - break; - } - - appendIntegral(hash, key, 0xBF); - return true; -} - -bool appendCanonicalSignature( - const MDSignature* signature, MDSectionOffset signatureOffset, - const SignatureMap& signatures, - std::unordered_set* activeSignatures, uint64_t* hash, - std::string* key) { - if (signature == nullptr || hash == nullptr || activeSignatures == nullptr) { - return false; - } - - const bool trackRecursion = signatureOffset != MD_SECTION_OFFSET_NULL; - if (trackRecursion) { - if (activeSignatures->find(signatureOffset) != activeSignatures->end()) { - appendIntegral(hash, key, 0xEE); - return true; - } - activeSignatures->insert(signatureOffset); - } - - appendIntegral(hash, key, 0xA0); - appendIntegral(hash, key, signature->isVariadic ? 1 : 0); - - if (!appendCanonicalType(signature->returnType, signatures, activeSignatures, - hash, key)) { - if (trackRecursion) { - activeSignatures->erase(signatureOffset); - } - return false; - } - - uint32_t argCount = 0; - for (const auto* arg : signature->arguments) { - if (!appendCanonicalType(arg, signatures, activeSignatures, hash, key)) { - if (trackRecursion) { - activeSignatures->erase(signatureOffset); - } - return false; - } - argCount++; - } - - appendIntegral(hash, key, argCount); - appendIntegral(hash, key, 0xAF); - - if (trackRecursion) { - activeSignatures->erase(signatureOffset); - } - return true; -} - -uint64_t signatureHash(const MDSignature* signature, - MDSectionOffset signatureOffset, - const SignatureMap& signatures, - std::string* canonicalKeyOut) { - if (signature == nullptr) { - return 0; - } - - uint64_t hash = kFNV64OffsetBasis; - std::unordered_set activeSignatures; - if (!appendCanonicalSignature(signature, signatureOffset, signatures, - &activeSignatures, &hash, canonicalKeyOut)) { - return 0; - } - return hash; -} - -bool mapTypeToCpp(const MDTypeInfo* type, std::string* out, - bool allowVoid = false); - -bool mapPointerPointeeToCpp(const MDTypeInfo* type, std::string* out) { - if (type == nullptr || out == nullptr) { - return false; - } - - switch (type->kind) { - case mdTypeVoid: - *out = "void"; - return true; - case mdTypePointer: { - std::string nested; - if (!mapPointerPointeeToCpp(type->pointeeType, &nested)) { - return false; - } - *out = nested + "*"; - return true; - } - default: - return mapTypeToCpp(type, out, false); - } -} - -bool mapTypeToCpp(const MDTypeInfo* type, std::string* out, bool allowVoid) { - if (type == nullptr || out == nullptr) { - return false; - } - - switch (type->kind) { - case mdTypeVoid: - if (!allowVoid) { - return false; - } - *out = "void"; - return true; - - case mdTypeBool: - *out = "uint8_t"; - return true; - - case mdTypeChar: - *out = "int8_t"; - return true; - - case mdTypeUChar: - case mdTypeUInt8: - *out = "uint8_t"; - return true; - - case mdTypeSShort: - *out = "int16_t"; - return true; - - case mdTypeUShort: - *out = "uint16_t"; - return true; - - case mdTypeSInt: - *out = "int32_t"; - return true; - - case mdTypeUInt: - *out = "uint32_t"; - return true; - - case mdTypeSLong: - case mdTypeSInt64: - *out = "int64_t"; - return true; - - case mdTypeULong: - case mdTypeUInt64: - *out = "uint64_t"; - return true; - - case mdTypeFloat: - *out = "float"; - return true; - - case mdTypeDouble: - *out = "double"; - return true; - - case mdTypeString: - *out = "char*"; - return true; - - case mdTypeAnyObject: - case mdTypeProtocolObject: - case mdTypeClassObject: - case mdTypeInstanceObject: - case mdTypeNSStringObject: - case mdTypeNSMutableStringObject: - *out = "id"; - return true; - - case mdTypeClass: - *out = "Class"; - return true; - - case mdTypeSelector: - *out = "SEL"; - return true; - - case mdTypePointer: { - std::string pointee; - if (!mapPointerPointeeToCpp(type->pointeeType, &pointee)) { - return false; - } - *out = pointee + "*"; - return true; - } - - case mdTypeOpaquePointer: - case mdTypeBlock: - case mdTypeFunctionPointer: - *out = "void*"; - return true; - - default: - return false; - } -} - -bool isSignatureSupported(const MDSignature* signature) { - if (signature == nullptr || signature->isVariadic) { - return false; - } - // Keep generated dispatch focused on hot paths and avoid huge wrappers. - if (signature->arguments.size() > 8) { - return false; - } - - std::string unused; - if (!mapTypeToCpp(signature->returnType, &unused, true)) { - return false; - } - - for (const auto* arg : signature->arguments) { - if (!mapTypeToCpp(arg, &unused, false)) { - return false; - } - } - - return true; -} - -std::string toHexLiteral(uint64_t value) { - std::ostringstream stream; - stream << "0x" << std::hex << std::setw(16) << std::setfill('0') << value - << "ULL"; - return stream.str(); -} - -std::string toBase36(size_t value) { - std::ostringstream stream; - if (value == 0) { - stream << '0'; - return stream.str(); - } - std::string digits; - while (value > 0) { - const size_t digit = value % 36; - digits.push_back( - static_cast(digit < 10 ? ('0' + digit) : ('a' + digit - 10))); - value /= 36; - } - std::reverse(digits.begin(), digits.end()); - stream << digits; - return stream.str(); -} - -std::string makeNapiWrapperName(DispatchKind kind, size_t index) { - std::ostringstream stream; - stream << "d" << (kind == DispatchKind::ObjCMethod ? "o" : "c") - << toBase36(index); - return stream.str(); -} - -bool isFastDirectNapiKind(MDTypeKind kind) { - switch (kind) { - case mdTypeBool: - case mdTypeChar: - case mdTypeUChar: - case mdTypeUInt8: - case mdTypeSShort: - case mdTypeUShort: - case mdTypeSInt: - case mdTypeUInt: - case mdTypeSLong: - case mdTypeULong: - case mdTypeSInt64: - case mdTypeUInt64: - case mdTypeFloat: - case mdTypeDouble: - return true; - default: - return false; - } -} - -bool isFastManagedNapiKind(MDTypeKind kind) { - switch (kind) { - case mdTypeAnyObject: - case mdTypeProtocolObject: - case mdTypeClassObject: - case mdTypeInstanceObject: - case mdTypeNSStringObject: - case mdTypeNSMutableStringObject: - case mdTypeSelector: - return true; - default: - return false; - } -} - -bool argKindMayNeedCleanup(MDTypeKind kind) { - switch (kind) { - case mdTypeAnyObject: - case mdTypeProtocolObject: - case mdTypeClassObject: - case mdTypeInstanceObject: - case mdTypeNSStringObject: - case mdTypeNSMutableStringObject: - case mdTypeClass: - case mdTypeSelector: - return false; - default: - return !isFastDirectNapiKind(kind); - } -} - -std::string makeWrapperShapeKey(DispatchKind kind, - const MDSignature* signature) { - if (signature == nullptr) { - return {}; - } - - std::string returnType; - if (!mapTypeToCpp(signature->returnType, &returnType, true)) { - return {}; - } - - std::ostringstream key; - key << static_cast(kind) << "|" << returnType << "|"; - for (const auto* arg : signature->arguments) { - std::string argType; - if (!mapTypeToCpp(arg, &argType, false)) { - return {}; - } - - if (isFastDirectNapiKind(arg->kind)) { - key << "F" << static_cast(arg->kind); - } else if (isFastManagedNapiKind(arg->kind)) { - key << "H" << static_cast(arg->kind); - } else { - key << "M" << argType; - } - key << "|"; - } - - return key.str(); -} - -void writeFastNapiArgConversion(std::ostringstream& out, const MDTypeInfo* type, - size_t index, bool hasCleanupArgs) { - const char* failCleanup = hasCleanupArgs ? " cleanupManagedArgs();\n" : ""; - if (type == nullptr) { - out << failCleanup; - out << " return false;\n"; - return; - } - - switch (type->kind) { - case mdTypeChar: { - out << " int32_t tmpArg" << index << " = 0;\n"; - out << " if (napi_get_value_int32(env, argv[" << index << "], &tmpArg" - << index << ") != napi_ok) {\n"; - if (hasCleanupArgs) { - out << " cleanupManagedArgs();\n"; - } - out << " return false;\n"; - out << " }\n"; - out << " arg" << index << " = static_cast(tmpArg" << index - << ");\n"; - break; - } - case mdTypeUChar: - case mdTypeUInt8: { - out << " uint32_t tmpArg" << index << " = 0;\n"; - out << " if (napi_get_value_uint32(env, argv[" << index << "], &tmpArg" - << index << ") != napi_ok) {\n"; - if (hasCleanupArgs) { - out << " cleanupManagedArgs();\n"; - } - out << " return false;\n"; - out << " }\n"; - out << " arg" << index << " = static_cast(tmpArg" << index - << ");\n"; - break; - } - case mdTypeSShort: { - out << " int32_t tmpArg" << index << " = 0;\n"; - out << " if (napi_get_value_int32(env, argv[" << index << "], &tmpArg" - << index << ") != napi_ok) {\n"; - if (hasCleanupArgs) { - out << " cleanupManagedArgs();\n"; - } - out << " return false;\n"; - out << " }\n"; - out << " arg" << index << " = static_cast(tmpArg" << index - << ");\n"; - break; - } - case mdTypeUShort: { - out << " if (!TryFastConvertNapiUInt16Argument(env, argv[" << index - << "], &arg" << index << ")) {\n"; - if (hasCleanupArgs) { - out << " cleanupManagedArgs();\n"; - } - out << " return false;\n"; - out << " }\n"; - break; - } - case mdTypeSInt: { - out << " if (napi_get_value_int32(env, argv[" << index << "], &arg" - << index << ") != napi_ok) {\n"; - if (hasCleanupArgs) { - out << " cleanupManagedArgs();\n"; - } - out << " return false;\n"; - out << " }\n"; - break; - } - case mdTypeUInt: { - out << " if (napi_get_value_uint32(env, argv[" << index << "], &arg" - << index << ") != napi_ok) {\n"; - if (hasCleanupArgs) { - out << " cleanupManagedArgs();\n"; - } - out << " return false;\n"; - out << " }\n"; - break; - } - case mdTypeSLong: - case mdTypeSInt64: { - out << " if (napi_get_value_int64(env, argv[" << index << "], &arg" - << index << ") != napi_ok) {\n"; - out << " bool lossless" << index << " = false;\n"; - out << " if (napi_get_value_bigint_int64(env, argv[" << index - << "], &arg" << index << ", &lossless" << index - << ") != napi_ok) {\n"; - if (hasCleanupArgs) { - out << " cleanupManagedArgs();\n"; - } - out << " return false;\n"; - out << " }\n"; - out << " }\n"; - break; - } - case mdTypeULong: - case mdTypeUInt64: { - out << " bool lossless" << index << " = false;\n"; - out << " if (napi_get_value_bigint_uint64(env, argv[" << index - << "], &arg" << index << ", &lossless" << index - << ") != napi_ok) {\n"; - out << " int64_t signedValue" << index << " = 0;\n"; - out << " if (napi_get_value_int64(env, argv[" << index - << "], &signedValue" << index << ") != napi_ok) {\n"; - if (hasCleanupArgs) { - out << " cleanupManagedArgs();\n"; - } - out << " return false;\n"; - out << " }\n"; - out << " arg" << index << " = static_cast(signedValue" - << index << ");\n"; - out << " }\n"; - break; - } - case mdTypeFloat: { - out << " double tmpArg" << index << " = 0.0;\n"; - out << " if (napi_get_value_double(env, argv[" << index << "], &tmpArg" - << index << ") != napi_ok) {\n"; - if (hasCleanupArgs) { - out << " cleanupManagedArgs();\n"; - } - out << " return false;\n"; - out << " }\n"; - out << " arg" << index << " = static_cast(tmpArg" << index - << ");\n"; - break; - } - case mdTypeDouble: { - out << " if (napi_get_value_double(env, argv[" << index << "], &arg" - << index << ") != napi_ok) {\n"; - if (hasCleanupArgs) { - out << " cleanupManagedArgs();\n"; - } - out << " return false;\n"; - out << " }\n"; - out << " if (std::isnan(arg" << index << ") || std::isinf(arg" << index - << ")) {\n"; - out << " arg" << index << " = 0.0;\n"; - out << " }\n"; - break; - } - case mdTypeBool: { - out << " bool boolValue" << index << " = false;\n"; - out << " if (napi_get_value_bool(env, argv[" << index << "], &boolValue" - << index << ") != napi_ok) {\n"; - if (hasCleanupArgs) { - out << " cleanupManagedArgs();\n"; - } - out << " return false;\n"; - out << " }\n"; - out << " arg" << index << " = static_cast(boolValue" << index - << " ? 1 : 0);\n"; - break; - } - default: - out << failCleanup; - out << " return false;\n"; - break; - } -} - -void writeNapiWrapper(std::ostringstream& out, DispatchKind kind, - const std::string& wrapperName, - const MDSignature* signature) { - std::string returnType; - if (!mapTypeToCpp(signature->returnType, &returnType, true)) { - return; - } - - std::vector argTypeInfos; - std::vector argTypes; - argTypes.reserve(signature->arguments.size()); - argTypeInfos.reserve(signature->arguments.size()); - for (const auto* arg : signature->arguments) { - std::string argType; - if (!mapTypeToCpp(arg, &argType, false)) { - return; - } - argTypeInfos.push_back(arg); - argTypes.push_back(argType); - } - - out << "static inline bool " << wrapperName - << "(napi_env env, Cif* cif, void* fnptr, "; - if (kind == DispatchKind::ObjCMethod) { - out << "id self, SEL selector, "; - } - out << "const napi_value* argv, void* rvalue) {\n"; - - out << " using Fn = " << returnType << " (*)("; - bool first = true; - if (kind == DispatchKind::ObjCMethod) { - out << "id, SEL"; - first = false; - } - for (const auto& argType : argTypes) { - if (!first) { - out << ", "; - } - out << argType; - first = false; - } - out << ");\n"; - out << " auto fn = reinterpret_cast(fnptr);\n"; - std::vector cleanupArgIndexes; - std::vector noCleanupManagedArgIndexes; - cleanupArgIndexes.reserve(argTypes.size()); - noCleanupManagedArgIndexes.reserve(argTypes.size()); - for (size_t i = 0; i < argTypes.size(); i++) { - if (!isFastDirectNapiKind(argTypeInfos[i]->kind)) { - if (argKindMayNeedCleanup(argTypeInfos[i]->kind)) { - cleanupArgIndexes.push_back(i); - } else { - noCleanupManagedArgIndexes.push_back(i); - } - } - } - const bool hasCleanupArgs = !cleanupArgIndexes.empty(); - if (hasCleanupArgs) { - out << " bool shouldFreeAny = false;\n"; - } - if (!noCleanupManagedArgIndexes.empty()) { - out << " bool ignoredShouldFree = false;\n"; - out << " bool ignoredShouldFreeAny = false;\n"; - } - if (returnType != "void") { - out << " " << returnType << " nativeResult{};\n"; - } - - for (size_t i = 0; i < argTypes.size(); i++) { - out << " " << argTypes[i] << " arg" << i << "{};\n"; - if (!isFastDirectNapiKind(argTypeInfos[i]->kind) && - argKindMayNeedCleanup(argTypeInfos[i]->kind)) { - out << " bool shouldFree" << i << " = false;\n"; - } - } - - if (hasCleanupArgs) { - out << " auto cleanupManagedArgs = [&]() {\n"; - out << " if (shouldFreeAny) {\n"; - if (kind == DispatchKind::CFunction && returnType != "void") { - out << " void* returnPointerValue = nullptr;\n"; - out << " if (cif->returnType != nullptr && cif->returnType->type == " - "&ffi_type_pointer) {\n"; - out << " returnPointerValue = " - "*reinterpret_cast(&nativeResult);\n"; - out << " }\n"; - } - for (const auto i : cleanupArgIndexes) { - out << " if (shouldFree" << i << ") {\n"; - if (kind == DispatchKind::CFunction && returnType != "void") { - out << " if (returnPointerValue != nullptr && " - "*reinterpret_cast(&arg" - << i << ") == returnPointerValue) {\n"; - out << " // Returning an argument pointer keeps ownership " - "with " - "the return value.\n"; - out << " } else {\n"; - out << " cif->argTypes[" << i - << "]->free(env, *reinterpret_cast(&arg" << i << "));\n"; - out << " }\n"; - } else { - out << " cif->argTypes[" << i - << "]->free(env, *reinterpret_cast(&arg" << i << "));\n"; - } - out << " }\n"; - } - out << " }\n"; - out << " };\n"; - } - - for (size_t i = 0; i < argTypes.size(); i++) { - if (isFastDirectNapiKind(argTypeInfos[i]->kind)) { - writeFastNapiArgConversion(out, argTypeInfos[i], i, hasCleanupArgs); - } else if (isFastManagedNapiKind(argTypeInfos[i]->kind)) { - out << " if (!TryFastConvertNapiArgument(env, static_cast(" - << static_cast(argTypeInfos[i]->kind) << "), argv[" << i - << "], &arg" << i << ")) {\n"; - if (argKindMayNeedCleanup(argTypeInfos[i]->kind)) { - out << " cif->argTypes[" << i << "]->toNative(env, argv[" << i - << "], &arg" << i << ", &shouldFree" << i << ", &shouldFreeAny);\n"; - } else { - out << " ignoredShouldFree = false;\n"; - out << " ignoredShouldFreeAny = false;\n"; - out << " cif->argTypes[" << i << "]->toNative(env, argv[" << i - << "], &arg" << i - << ", &ignoredShouldFree, &ignoredShouldFreeAny);\n"; - } - out << " }\n"; - } else { - if (argKindMayNeedCleanup(argTypeInfos[i]->kind)) { - out << " cif->argTypes[" << i << "]->toNative(env, argv[" << i - << "], &arg" << i << ", &shouldFree" << i << ", &shouldFreeAny);\n"; - } else { - out << " ignoredShouldFree = false;\n"; - out << " ignoredShouldFreeAny = false;\n"; - out << " cif->argTypes[" << i << "]->toNative(env, argv[" << i - << "], &arg" << i - << ", &ignoredShouldFree, &ignoredShouldFreeAny);\n"; - } - } - } - - std::ostringstream callExpr; - callExpr << "fn("; - bool hasAnyCallArg = false; - if (kind == DispatchKind::ObjCMethod) { - callExpr << "self, selector"; - hasAnyCallArg = true; - } - for (size_t i = 0; i < argTypes.size(); i++) { - if (hasAnyCallArg) { - callExpr << ", "; - } - callExpr << "arg" << i; - hasAnyCallArg = true; - } - callExpr << ")"; - - if (returnType == "void") { - out << " " << callExpr.str() << ";\n"; - } else { - out << " nativeResult = " << callExpr.str() << ";\n"; - out << " *reinterpret_cast<" << returnType - << "*>(rvalue) = nativeResult;\n"; - } - if (hasCleanupArgs) { - out << " cleanupManagedArgs();\n"; - } - - out << " return true;\n"; - out << "}\n\n"; -} - -void collectMethodUses(const std::vector& members, - std::vector* uses) { - if (uses == nullptr) { - return; - } - - for (auto* member : members) { - if (member == nullptr) { - continue; - } - - const uint8_t methodFlags = - (member->flags & mdMemberReturnOwned) != 0 ? 1 : 0; - - if ((member->flags & mdMemberProperty) != 0) { - if (member->getterSignature != MD_SECTION_OFFSET_NULL) { - uses->push_back( - {DispatchKind::ObjCMethod, member->getterSignature, methodFlags}); - } - if (((member->flags & mdMemberReadonly) == 0) && - member->setterSignature != MD_SECTION_OFFSET_NULL) { - uses->push_back({DispatchKind::ObjCMethod, member->setterSignature, 0}); - } - } else { - if (member->signature != MD_SECTION_OFFSET_NULL) { - uses->push_back( - {DispatchKind::ObjCMethod, member->signature, methodFlags}); - } - } - } -} - -} // namespace +using namespace signature_dispatch; void writeSignatureDispatchBindings(const MDMetadataWriter& writer, const std::string& outputPath) { @@ -879,10 +48,29 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, collectMethodUses(protocol->members, &signatureUses); } + const auto rootSignatureUses = signatureUses; + std::unordered_set activeBlockSignatures; + for (const auto& use : rootSignatureUses) { + collectBlockUsesFromSignature(use.signatureOffset, writer.signatures, + &activeBlockSignatures, &signatureUses); + } + std::unordered_map> wrappersByKey; + std::unordered_map> + preparedWrappersByKey; std::unordered_map objcNapiEntries; std::unordered_map cFunctionNapiEntries; + std::unordered_map objcEngineDirectEntries; + std::unordered_map cFunctionEngineDirectEntries; + std::unordered_map objcV8Entries; + std::unordered_map cFunctionV8Entries; + std::unordered_map objcHermesDirectReturnEntries; + std::unordered_map cFunctionHermesDirectReturnEntries; + std::unordered_map objcHermesFrameDirectReturnEntries; + std::unordered_map cFunctionHermesFrameDirectReturnEntries; + std::unordered_map blockHermesFrameDirectReturnEntries; + std::unordered_map blockPreparedEntries; std::unordered_map dispatchEncoding; std::unordered_set collidedDispatchIds; @@ -912,6 +100,16 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, collidedDispatchIds.insert(dispatchId); objcNapiEntries.erase(dispatchId); cFunctionNapiEntries.erase(dispatchId); + objcEngineDirectEntries.erase(dispatchId); + cFunctionEngineDirectEntries.erase(dispatchId); + objcV8Entries.erase(dispatchId); + cFunctionV8Entries.erase(dispatchId); + objcHermesDirectReturnEntries.erase(dispatchId); + cFunctionHermesDirectReturnEntries.erase(dispatchId); + objcHermesFrameDirectReturnEntries.erase(dispatchId); + cFunctionHermesFrameDirectReturnEntries.erase(dispatchId); + blockHermesFrameDirectReturnEntries.erase(dispatchId); + blockPreparedEntries.erase(dispatchId); dispatchEncoding.erase(dispatchId); continue; } @@ -924,12 +122,41 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, if (wrapperKey.empty()) { continue; } - wrappersByKey.emplace(wrapperKey, std::make_pair(use.kind, signature)); if (use.kind == DispatchKind::ObjCMethod) { + wrappersByKey.emplace(wrapperKey, std::make_pair(use.kind, signature)); objcNapiEntries.emplace(dispatchId, wrapperKey); - } else { + objcEngineDirectEntries.emplace(dispatchId, wrapperKey); + objcV8Entries.emplace(dispatchId, wrapperKey); + if (canUseHermesDirectReturnWrapper( + use.kind, signature, HermesDirectReturnCallSite::FastCallback)) { + objcHermesDirectReturnEntries.emplace(dispatchId, wrapperKey); + } + if (canUseHermesDirectReturnWrapper( + use.kind, signature, HermesDirectReturnCallSite::Frame)) { + objcHermesFrameDirectReturnEntries.emplace(dispatchId, wrapperKey); + } + } else if (use.kind == DispatchKind::CFunction) { + wrappersByKey.emplace(wrapperKey, std::make_pair(use.kind, signature)); cFunctionNapiEntries.emplace(dispatchId, wrapperKey); + cFunctionEngineDirectEntries.emplace(dispatchId, wrapperKey); + cFunctionV8Entries.emplace(dispatchId, wrapperKey); + if (canUseHermesDirectReturnWrapper( + use.kind, signature, HermesDirectReturnCallSite::FastCallback)) { + cFunctionHermesDirectReturnEntries.emplace(dispatchId, wrapperKey); + } + if (canUseHermesDirectReturnWrapper( + use.kind, signature, HermesDirectReturnCallSite::Frame)) { + cFunctionHermesFrameDirectReturnEntries.emplace(dispatchId, wrapperKey); + } + } else if (use.kind == DispatchKind::BlockInvoke) { + preparedWrappersByKey.emplace(wrapperKey, + std::make_pair(use.kind, signature)); + blockPreparedEntries.emplace(dispatchId, wrapperKey); + if (canUseHermesDirectReturnWrapper( + use.kind, signature, HermesDirectReturnCallSite::Frame)) { + blockHermesFrameDirectReturnEntries.emplace(dispatchId, wrapperKey); + } } } @@ -940,6 +167,14 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, wrappers.begin(), wrappers.end(), [](const auto& lhs, const auto& rhs) { return lhs.first < rhs.first; }); + std::vector< + std::pair>> + preparedWrappers(preparedWrappersByKey.begin(), + preparedWrappersByKey.end()); + std::sort( + preparedWrappers.begin(), preparedWrappers.end(), + [](const auto& lhs, const auto& rhs) { return lhs.first < rhs.first; }); + std::unordered_map wrapperNameByKey; wrapperNameByKey.reserve(wrappers.size()); size_t wrapperIndex = 0; @@ -949,6 +184,65 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, makeNapiWrapperName(wrapper.second.first, wrapperIndex++)); } + std::unordered_map v8WrapperNameByKey; + v8WrapperNameByKey.reserve(wrappers.size()); + size_t v8WrapperIndex = 0; + for (const auto& wrapper : wrappers) { + v8WrapperNameByKey.emplace( + wrapper.first, + makeV8WrapperName(wrapper.second.first, v8WrapperIndex++)); + } + + std::unordered_map hermesDirectReturnWrapperNameByKey; + hermesDirectReturnWrapperNameByKey.reserve(wrappers.size()); + size_t hermesDirectReturnWrapperIndex = 0; + for (const auto& wrapper : wrappers) { + hermesDirectReturnWrapperNameByKey.emplace( + wrapper.first, + makeHermesDirectReturnWrapperName(wrapper.second.first, + hermesDirectReturnWrapperIndex++)); + } + + std::unordered_map + hermesFrameDirectReturnWrapperNameByKey; + hermesFrameDirectReturnWrapperNameByKey.reserve(wrappers.size()); + size_t hermesFrameDirectReturnWrapperIndex = 0; + for (const auto& wrapper : wrappers) { + hermesFrameDirectReturnWrapperNameByKey.emplace( + wrapper.first, + makeHermesFrameDirectReturnWrapperName( + wrapper.second.first, hermesFrameDirectReturnWrapperIndex++)); + } + + std::unordered_map + hermesBlockFrameDirectReturnWrapperNameByKey; + hermesBlockFrameDirectReturnWrapperNameByKey.reserve(preparedWrappers.size()); + for (const auto& wrapper : preparedWrappers) { + hermesBlockFrameDirectReturnWrapperNameByKey.emplace( + wrapper.first, + makeHermesFrameDirectReturnWrapperName( + wrapper.second.first, hermesFrameDirectReturnWrapperIndex++)); + } + + std::unordered_map engineDirectWrapperNameByKey; + engineDirectWrapperNameByKey.reserve(wrappers.size()); + size_t engineDirectWrapperIndex = 0; + for (const auto& wrapper : wrappers) { + engineDirectWrapperNameByKey.emplace( + wrapper.first, + makeEngineDirectWrapperName(wrapper.second.first, + engineDirectWrapperIndex++)); + } + + std::unordered_map preparedWrapperNameByKey; + preparedWrapperNameByKey.reserve(preparedWrappers.size()); + size_t preparedWrapperIndex = 0; + for (const auto& wrapper : preparedWrappers) { + preparedWrapperNameByKey.emplace( + wrapper.first, + makePreparedWrapperName(wrapper.second.first, preparedWrapperIndex++)); + } + std::vector> sortedObjCNapiEntries( objcNapiEntries.begin(), objcNapiEntries.end()); std::sort( @@ -961,20 +255,276 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, sortedCFunctionNapiEntries.begin(), sortedCFunctionNapiEntries.end(), [](const auto& lhs, const auto& rhs) { return lhs.first < rhs.first; }); + std::vector> sortedObjCEngineDirectEntries( + objcEngineDirectEntries.begin(), objcEngineDirectEntries.end()); + std::sort(sortedObjCEngineDirectEntries.begin(), + sortedObjCEngineDirectEntries.end(), + [](const auto& lhs, const auto& rhs) { + return lhs.first < rhs.first; + }); + + std::vector> + sortedCFunctionEngineDirectEntries(cFunctionEngineDirectEntries.begin(), + cFunctionEngineDirectEntries.end()); + std::sort(sortedCFunctionEngineDirectEntries.begin(), + sortedCFunctionEngineDirectEntries.end(), + [](const auto& lhs, const auto& rhs) { + return lhs.first < rhs.first; + }); + + std::vector> sortedObjCV8Entries( + objcV8Entries.begin(), objcV8Entries.end()); + std::sort( + sortedObjCV8Entries.begin(), sortedObjCV8Entries.end(), + [](const auto& lhs, const auto& rhs) { return lhs.first < rhs.first; }); + + std::vector> sortedCFunctionV8Entries( + cFunctionV8Entries.begin(), cFunctionV8Entries.end()); + std::sort( + sortedCFunctionV8Entries.begin(), sortedCFunctionV8Entries.end(), + [](const auto& lhs, const auto& rhs) { return lhs.first < rhs.first; }); + + std::vector> + sortedObjCHermesDirectReturnEntries( + objcHermesDirectReturnEntries.begin(), + objcHermesDirectReturnEntries.end()); + std::sort(sortedObjCHermesDirectReturnEntries.begin(), + sortedObjCHermesDirectReturnEntries.end(), + [](const auto& lhs, const auto& rhs) { + return lhs.first < rhs.first; + }); + + std::vector> + sortedCFunctionHermesDirectReturnEntries( + cFunctionHermesDirectReturnEntries.begin(), + cFunctionHermesDirectReturnEntries.end()); + std::sort(sortedCFunctionHermesDirectReturnEntries.begin(), + sortedCFunctionHermesDirectReturnEntries.end(), + [](const auto& lhs, const auto& rhs) { + return lhs.first < rhs.first; + }); + + std::vector> + sortedObjCHermesFrameDirectReturnEntries( + objcHermesFrameDirectReturnEntries.begin(), + objcHermesFrameDirectReturnEntries.end()); + std::sort(sortedObjCHermesFrameDirectReturnEntries.begin(), + sortedObjCHermesFrameDirectReturnEntries.end(), + [](const auto& lhs, const auto& rhs) { + return lhs.first < rhs.first; + }); + + std::vector> + sortedCFunctionHermesFrameDirectReturnEntries( + cFunctionHermesFrameDirectReturnEntries.begin(), + cFunctionHermesFrameDirectReturnEntries.end()); + std::sort(sortedCFunctionHermesFrameDirectReturnEntries.begin(), + sortedCFunctionHermesFrameDirectReturnEntries.end(), + [](const auto& lhs, const auto& rhs) { + return lhs.first < rhs.first; + }); + + std::vector> + sortedBlockHermesFrameDirectReturnEntries( + blockHermesFrameDirectReturnEntries.begin(), + blockHermesFrameDirectReturnEntries.end()); + std::sort(sortedBlockHermesFrameDirectReturnEntries.begin(), + sortedBlockHermesFrameDirectReturnEntries.end(), + [](const auto& lhs, const auto& rhs) { + return lhs.first < rhs.first; + }); + + std::vector> sortedBlockPreparedEntries( + blockPreparedEntries.begin(), blockPreparedEntries.end()); + std::sort( + sortedBlockPreparedEntries.begin(), sortedBlockPreparedEntries.end(), + [](const auto& lhs, const auto& rhs) { return lhs.first < rhs.first; }); + std::ostringstream generated; generated << "#ifndef NS_GENERATED_SIGNATURE_DISPATCH_INC\n"; generated << "#define NS_GENERATED_SIGNATURE_DISPATCH_INC\n\n"; + generated << "#if NS_GSD_BACKEND_V8 || NS_GSD_BACKEND_NAPI || " + "NS_GSD_BACKEND_ENGINE_DIRECT\n"; generated << "#undef NS_HAS_GENERATED_SIGNATURE_DISPATCH\n"; - generated << "#define NS_HAS_GENERATED_SIGNATURE_DISPATCH 0\n"; + generated << "#define NS_HAS_GENERATED_SIGNATURE_DISPATCH 1\n"; + generated << "#endif\n"; + generated << "#if NS_GSD_BACKEND_NAPI\n"; generated << "#undef NS_HAS_GENERATED_SIGNATURE_NAPI_DISPATCH\n"; - generated << "#define NS_HAS_GENERATED_SIGNATURE_NAPI_DISPATCH 1\n\n"; + generated << "#define NS_HAS_GENERATED_SIGNATURE_NAPI_DISPATCH 1\n"; + generated << "#endif\n"; + generated << "#if NS_GSD_BACKEND_V8\n"; + generated << "#undef NS_HAS_GENERATED_SIGNATURE_V8_DISPATCH\n"; + generated << "#define NS_HAS_GENERATED_SIGNATURE_V8_DISPATCH 1\n"; + generated << "#endif\n"; + generated << "#if NS_GSD_BACKEND_ENGINE_DIRECT\n"; + generated << "#undef NS_HAS_GENERATED_SIGNATURE_ENGINE_DIRECT_DISPATCH\n"; + generated << "#define NS_HAS_GENERATED_SIGNATURE_ENGINE_DIRECT_DISPATCH 1\n"; + generated << "#endif\n\n"; + generated << "#if NS_GSD_BACKEND_HERMES\n"; + generated << "#undef NS_HAS_GENERATED_SIGNATURE_HERMES_DIRECT_RETURN_DISPATCH\n"; + generated << "#define NS_HAS_GENERATED_SIGNATURE_HERMES_DIRECT_RETURN_DISPATCH 1\n"; + generated << "#undef " + "NS_HAS_GENERATED_SIGNATURE_HERMES_FRAME_DIRECT_RETURN_DISPATCH\n"; + generated << "#define " + "NS_HAS_GENERATED_SIGNATURE_HERMES_FRAME_DIRECT_RETURN_DISPATCH 1\n"; + generated << "#undef " + "NS_HAS_GENERATED_SIGNATURE_HERMES_BLOCK_FRAME_DIRECT_RETURN_DISPATCH\n"; + generated << "#define " + "NS_HAS_GENERATED_SIGNATURE_HERMES_BLOCK_FRAME_DIRECT_RETURN_DISPATCH 1\n"; + generated << "#endif\n\n"; generated << "namespace nativescript {\n\n"; + generated << "#if NS_GSD_BACKEND_V8 || NS_GSD_BACKEND_NAPI || " + "NS_GSD_BACKEND_ENGINE_DIRECT\n"; + for (const auto& wrapper : preparedWrappers) { + writePreparedWrapper(generated, wrapper.second.first, + preparedWrapperNameByKey.at(wrapper.first), + wrapper.second.second); + } + generated << "#endif\n\n"; + + generated << "#if NS_GSD_BACKEND_NAPI\n"; for (const auto& wrapper : wrappers) { writeNapiWrapper(generated, wrapper.second.first, wrapperNameByKey.at(wrapper.first), wrapper.second.second); } + generated << "#endif\n\n"; + + generated << "#if NS_GSD_BACKEND_ENGINE_DIRECT\n"; + writeEngineDirectConverterMacros(generated); + for (const auto& wrapper : wrappers) { + writeEngineDirectWrapper(generated, wrapper.second.first, + engineDirectWrapperNameByKey.at(wrapper.first), + wrapper.second.second); + } + writeEngineDirectConverterUndefs(generated); + generated << "#endif\n\n"; + + generated << "#if NS_GSD_BACKEND_HERMES\n"; + writeHermesEngineDirectConverterMacros(generated); + for (const auto& wrapper : wrappers) { + writeHermesDirectReturnWrapper( + generated, wrapper.second.first, + hermesDirectReturnWrapperNameByKey.at(wrapper.first), + wrapper.second.second); + } + for (const auto& wrapper : wrappers) { + writeHermesFrameDirectReturnWrapper( + generated, wrapper.second.first, + hermesFrameDirectReturnWrapperNameByKey.at(wrapper.first), + wrapper.second.second); + } + for (const auto& wrapper : preparedWrappers) { + writeHermesFrameDirectReturnWrapper( + generated, wrapper.second.first, + hermesBlockFrameDirectReturnWrapperNameByKey.at(wrapper.first), + wrapper.second.second); + } + writeEngineDirectConverterUndefs(generated); + generated << "#endif\n\n"; + + generated << "#if NS_GSD_BACKEND_V8\n"; + for (const auto& wrapper : wrappers) { + writeV8Wrapper(generated, wrapper.second.first, + v8WrapperNameByKey.at(wrapper.first), wrapper.second.second); + } + generated << "#endif\n\n"; + + generated << "#if NS_GSD_BACKEND_V8 || NS_GSD_BACKEND_NAPI || " + "NS_GSD_BACKEND_ENGINE_DIRECT\n"; + generated << "inline constexpr ObjCDispatchEntry " + "kGeneratedObjCDispatchEntries[] = {\n"; + generated << " {0, nullptr},\n"; + generated << "};\n\n"; + + generated << "inline constexpr CFunctionDispatchEntry " + "kGeneratedCFunctionDispatchEntries[] = {\n"; + generated << " {0, nullptr},\n"; + generated << "};\n\n"; + + generated << "inline constexpr BlockDispatchEntry " + "kGeneratedBlockDispatchEntries[] = {\n"; + generated << " {0, nullptr},\n"; + for (const auto& entry : sortedBlockPreparedEntries) { + generated << " {" << toHexLiteral(entry.first) << ", &" + << preparedWrapperNameByKey.at(entry.second) << "},\n"; + } + generated << "};\n\n"; + generated << "#endif\n\n"; + + generated << "#if NS_GSD_BACKEND_ENGINE_DIRECT\n"; + generated << "inline constexpr ObjCEngineDirectDispatchEntry " + "kGeneratedObjCEngineDirectDispatchEntries[] = {\n"; + generated << " {0, nullptr},\n"; + for (const auto& entry : sortedObjCEngineDirectEntries) { + generated << " {" << toHexLiteral(entry.first) << ", &" + << engineDirectWrapperNameByKey.at(entry.second) << "},\n"; + } + generated << "};\n\n"; + + generated << "inline constexpr CFunctionEngineDirectDispatchEntry " + "kGeneratedCFunctionEngineDirectDispatchEntries[] = {\n"; + generated << " {0, nullptr},\n"; + for (const auto& entry : sortedCFunctionEngineDirectEntries) { + generated << " {" << toHexLiteral(entry.first) << ", &" + << engineDirectWrapperNameByKey.at(entry.second) << "},\n"; + } + generated << "};\n"; + generated << "#endif\n\n"; + + generated << "#if NS_GSD_BACKEND_HERMES\n"; + generated << "inline constexpr ObjCHermesDirectReturnDispatchEntry " + "kGeneratedObjCHermesDirectReturnDispatchEntries[] = {\n"; + generated << " {0, nullptr},\n"; + for (const auto& entry : sortedObjCHermesDirectReturnEntries) { + generated << " {" << toHexLiteral(entry.first) << ", &" + << hermesDirectReturnWrapperNameByKey.at(entry.second) + << "},\n"; + } + generated << "};\n\n"; + + generated << "inline constexpr CFunctionHermesDirectReturnDispatchEntry " + "kGeneratedCFunctionHermesDirectReturnDispatchEntries[] = {\n"; + generated << " {0, nullptr},\n"; + for (const auto& entry : sortedCFunctionHermesDirectReturnEntries) { + generated << " {" << toHexLiteral(entry.first) << ", &" + << hermesDirectReturnWrapperNameByKey.at(entry.second) + << "},\n"; + } + generated << "};\n"; + + generated << "inline constexpr ObjCHermesFrameDirectReturnDispatchEntry " + "kGeneratedObjCHermesFrameDirectReturnDispatchEntries[] = {\n"; + generated << " {0, nullptr},\n"; + for (const auto& entry : sortedObjCHermesFrameDirectReturnEntries) { + generated << " {" << toHexLiteral(entry.first) << ", &" + << hermesFrameDirectReturnWrapperNameByKey.at(entry.second) + << "},\n"; + } + generated << "};\n\n"; + + generated << "inline constexpr CFunctionHermesFrameDirectReturnDispatchEntry " + "kGeneratedCFunctionHermesFrameDirectReturnDispatchEntries[] = {\n"; + generated << " {0, nullptr},\n"; + for (const auto& entry : sortedCFunctionHermesFrameDirectReturnEntries) { + generated << " {" << toHexLiteral(entry.first) << ", &" + << hermesFrameDirectReturnWrapperNameByKey.at(entry.second) + << "},\n"; + } + generated << "};\n"; + + generated << "inline constexpr BlockHermesFrameDirectReturnDispatchEntry " + "kGeneratedBlockHermesFrameDirectReturnDispatchEntries[] = {\n"; + generated << " {0, nullptr},\n"; + for (const auto& entry : sortedBlockHermesFrameDirectReturnEntries) { + generated << " {" << toHexLiteral(entry.first) << ", &" + << hermesBlockFrameDirectReturnWrapperNameByKey.at(entry.second) + << "},\n"; + } + generated << "};\n"; + generated << "#endif\n\n"; + generated << "#if NS_GSD_BACKEND_NAPI\n"; generated << "inline constexpr ObjCNapiDispatchEntry " "kGeneratedObjCNapiDispatchEntries[] = {\n"; generated << " {0, nullptr},\n"; @@ -992,6 +542,27 @@ void writeSignatureDispatchBindings(const MDMetadataWriter& writer, << wrapperNameByKey.at(entry.second) << "},\n"; } generated << "};\n\n"; + generated << "#endif\n\n"; + + generated << "#if NS_GSD_BACKEND_V8\n"; + generated << "inline constexpr ObjCV8DispatchEntry " + "kGeneratedObjCV8DispatchEntries[] = {\n"; + generated << " {0, nullptr},\n"; + for (const auto& entry : sortedObjCV8Entries) { + generated << " {" << toHexLiteral(entry.first) << ", &" + << v8WrapperNameByKey.at(entry.second) << "},\n"; + } + generated << "};\n\n"; + + generated << "inline constexpr CFunctionV8DispatchEntry " + "kGeneratedCFunctionV8DispatchEntries[] = {\n"; + generated << " {0, nullptr},\n"; + for (const auto& entry : sortedCFunctionV8Entries) { + generated << " {" << toHexLiteral(entry.first) << ", &" + << v8WrapperNameByKey.at(entry.second) << "},\n"; + } + generated << "};\n"; + generated << "#endif\n\n"; generated << "} // namespace nativescript\n\n"; generated << "#endif // NS_GENERATED_SIGNATURE_DISPATCH_INC\n"; diff --git a/metadata-generator/src/SignatureDispatchEmitter/EngineDirect.cpp b/metadata-generator/src/SignatureDispatchEmitter/EngineDirect.cpp new file mode 100644 index 00000000..820eb3fb --- /dev/null +++ b/metadata-generator/src/SignatureDispatchEmitter/EngineDirect.cpp @@ -0,0 +1,363 @@ +#include "SignatureDispatchEmitter/Shared.h" + +#include +#include +#include + +namespace metagen::signature_dispatch { + +std::string makeEngineDirectWrapperName(DispatchKind kind, size_t index) { + std::ostringstream stream; + stream << "de"; + switch (kind) { + case DispatchKind::ObjCMethod: + stream << "o"; + break; + case DispatchKind::CFunction: + stream << "c"; + break; + case DispatchKind::BlockInvoke: + stream << "b"; + break; + } + stream << toBase36(index); + return stream.str(); +} +const char* engineDirectConverterMacroForKind(MDTypeKind kind) { + switch (kind) { + case mdTypeBool: + return "NS_GSD_ENGINE_DIRECT_CONVERT_BOOL_ARGUMENT"; + case mdTypeChar: + return "NS_GSD_ENGINE_DIRECT_CONVERT_INT8_ARGUMENT"; + case mdTypeUChar: + case mdTypeUInt8: + return "NS_GSD_ENGINE_DIRECT_CONVERT_UINT8_ARGUMENT"; + case mdTypeSShort: + return "NS_GSD_ENGINE_DIRECT_CONVERT_INT16_ARGUMENT"; + case mdTypeUShort: + return "NS_GSD_ENGINE_DIRECT_CONVERT_UINT16_ARGUMENT"; + case mdTypeSInt: + return "NS_GSD_ENGINE_DIRECT_CONVERT_INT32_ARGUMENT"; + case mdTypeUInt: + return "NS_GSD_ENGINE_DIRECT_CONVERT_UINT32_ARGUMENT"; + case mdTypeSLong: + case mdTypeSInt64: + return "NS_GSD_ENGINE_DIRECT_CONVERT_INT64_ARGUMENT"; + case mdTypeULong: + case mdTypeUInt64: + return "NS_GSD_ENGINE_DIRECT_CONVERT_UINT64_ARGUMENT"; + case mdTypeFloat: + return "NS_GSD_ENGINE_DIRECT_CONVERT_FLOAT_ARGUMENT"; + case mdTypeDouble: + return "NS_GSD_ENGINE_DIRECT_CONVERT_DOUBLE_ARGUMENT"; + case mdTypeSelector: + return "NS_GSD_ENGINE_DIRECT_CONVERT_SELECTOR_ARGUMENT"; + case mdTypeClass: + case mdTypeAnyObject: + case mdTypeProtocolObject: + case mdTypeClassObject: + case mdTypeInstanceObject: + case mdTypeNSStringObject: + case mdTypeNSMutableStringObject: + return "NS_GSD_ENGINE_DIRECT_CONVERT_OBJECT_ARGUMENT"; + default: + return "NS_GSD_ENGINE_DIRECT_CONVERT_ARGUMENT"; + } +} + +bool engineDirectConverterTakesKind(MDTypeKind kind) { + switch (kind) { + case mdTypeClass: + case mdTypeAnyObject: + case mdTypeProtocolObject: + case mdTypeClassObject: + case mdTypeInstanceObject: + case mdTypeNSStringObject: + case mdTypeNSMutableStringObject: + return true; + default: + return engineDirectConverterMacroForKind(kind) == + std::string("NS_GSD_ENGINE_DIRECT_CONVERT_ARGUMENT"); + } +} +void writeEngineDirectArgConversion(std::ostringstream& out, + const MDTypeInfo* type, size_t index, + const std::string& valueExpr = "") { + if (type == nullptr) { + out << " return false;\n"; + return; + } + + const std::string argValue = + valueExpr.empty() ? "argv[" + std::to_string(index) + "]" : valueExpr; + const char* converter = engineDirectConverterMacroForKind(type->kind); + out << " if (!" << converter << "(env, "; + if (engineDirectConverterTakesKind(type->kind)) { + out << "static_cast(" << static_cast(type->kind) + << "), "; + } + out << argValue << ", &arg" << index << ")) {\n"; + if (argKindMayNeedCleanup(type->kind)) { + out << " cif->argTypes[" << index << "]->toNative(env, " << argValue + << ", &arg" << index << ", &shouldFree" << index + << ", &shouldFreeAny);\n"; + } else { + out << " bool ignoredShouldFree = false;\n"; + out << " bool ignoredShouldFreeAny = false;\n"; + out << " cif->argTypes[" << index << "]->toNative(env, " << argValue + << ", &arg" << index + << ", &ignoredShouldFree, &ignoredShouldFreeAny);\n"; + } + out << " }\n"; +} + +void writeEngineDirectConverterMacros(std::ostringstream& out) { + out << "#if NS_GSD_BACKEND_JSC\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_ARGUMENT " + "TryFastConvertJSCArgument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_BOOL_ARGUMENT " + "TryFastConvertJSCBoolArgument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT8_ARGUMENT " + "TryFastConvertJSCInt8Argument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT8_ARGUMENT " + "TryFastConvertJSCUInt8Argument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT16_ARGUMENT " + "TryFastConvertJSCInt16Argument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT16_ARGUMENT " + "TryFastConvertJSCUInt16Argument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT32_ARGUMENT " + "TryFastConvertJSCInt32Argument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT32_ARGUMENT " + "TryFastConvertJSCUInt32Argument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT64_ARGUMENT " + "TryFastConvertJSCInt64Argument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT64_ARGUMENT " + "TryFastConvertJSCUInt64Argument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_FLOAT_ARGUMENT " + "TryFastConvertJSCFloatArgument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_DOUBLE_ARGUMENT " + "TryFastConvertJSCDoubleArgument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_SELECTOR_ARGUMENT " + "TryFastConvertJSCSelectorArgument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_OBJECT_ARGUMENT " + "TryFastConvertJSCObjectArgument\n"; + out << "#elif NS_GSD_BACKEND_QUICKJS\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_ARGUMENT " + "TryFastConvertQuickJSArgument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_BOOL_ARGUMENT " + "TryFastConvertQuickJSBoolArgument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT8_ARGUMENT " + "TryFastConvertQuickJSInt8Argument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT8_ARGUMENT " + "TryFastConvertQuickJSUInt8Argument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT16_ARGUMENT " + "TryFastConvertQuickJSInt16Argument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT16_ARGUMENT " + "TryFastConvertQuickJSUInt16Argument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT32_ARGUMENT " + "TryFastConvertQuickJSInt32Argument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT32_ARGUMENT " + "TryFastConvertQuickJSUInt32Argument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT64_ARGUMENT " + "TryFastConvertQuickJSInt64Argument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT64_ARGUMENT " + "TryFastConvertQuickJSUInt64Argument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_FLOAT_ARGUMENT " + "TryFastConvertQuickJSFloatArgument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_DOUBLE_ARGUMENT " + "TryFastConvertQuickJSDoubleArgument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_SELECTOR_ARGUMENT " + "TryFastConvertQuickJSSelectorArgument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_OBJECT_ARGUMENT " + "TryFastConvertQuickJSObjectArgument\n"; + out << "#elif NS_GSD_BACKEND_HERMES\n"; + writeHermesEngineDirectConverterMacros(out); + out << "#else\n"; + out << "#error \"No generated signature engine-direct converter selected\"\n"; + out << "#endif\n"; +} + +void writeHermesEngineDirectConverterMacros(std::ostringstream& out) { + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_ARGUMENT " + "TryFastConvertHermesArgument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_BOOL_ARGUMENT " + "TryFastConvertHermesGeneratedBoolArgument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT8_ARGUMENT " + "TryFastConvertHermesGeneratedInt8Argument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT8_ARGUMENT " + "TryFastConvertHermesGeneratedUInt8Argument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT16_ARGUMENT " + "TryFastConvertHermesGeneratedInt16Argument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT16_ARGUMENT " + "TryFastConvertHermesGeneratedUInt16Argument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT32_ARGUMENT " + "TryFastConvertHermesGeneratedInt32Argument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT32_ARGUMENT " + "TryFastConvertHermesGeneratedUInt32Argument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_INT64_ARGUMENT " + "TryFastConvertHermesGeneratedInt64Argument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_UINT64_ARGUMENT " + "TryFastConvertHermesGeneratedUInt64Argument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_FLOAT_ARGUMENT " + "TryFastConvertHermesGeneratedFloatArgument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_DOUBLE_ARGUMENT " + "TryFastConvertHermesGeneratedDoubleArgument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_SELECTOR_ARGUMENT " + "TryFastConvertHermesSelectorArgument\n"; + out << "#define NS_GSD_ENGINE_DIRECT_CONVERT_OBJECT_ARGUMENT " + "TryFastConvertHermesObjectArgument\n"; +} + +void writeEngineDirectConverterUndefs(std::ostringstream& out) { + out << "#undef NS_GSD_ENGINE_DIRECT_CONVERT_ARGUMENT\n"; + out << "#undef NS_GSD_ENGINE_DIRECT_CONVERT_BOOL_ARGUMENT\n"; + out << "#undef NS_GSD_ENGINE_DIRECT_CONVERT_INT8_ARGUMENT\n"; + out << "#undef NS_GSD_ENGINE_DIRECT_CONVERT_UINT8_ARGUMENT\n"; + out << "#undef NS_GSD_ENGINE_DIRECT_CONVERT_INT16_ARGUMENT\n"; + out << "#undef NS_GSD_ENGINE_DIRECT_CONVERT_UINT16_ARGUMENT\n"; + out << "#undef NS_GSD_ENGINE_DIRECT_CONVERT_INT32_ARGUMENT\n"; + out << "#undef NS_GSD_ENGINE_DIRECT_CONVERT_UINT32_ARGUMENT\n"; + out << "#undef NS_GSD_ENGINE_DIRECT_CONVERT_INT64_ARGUMENT\n"; + out << "#undef NS_GSD_ENGINE_DIRECT_CONVERT_UINT64_ARGUMENT\n"; + out << "#undef NS_GSD_ENGINE_DIRECT_CONVERT_FLOAT_ARGUMENT\n"; + out << "#undef NS_GSD_ENGINE_DIRECT_CONVERT_DOUBLE_ARGUMENT\n"; + out << "#undef NS_GSD_ENGINE_DIRECT_CONVERT_SELECTOR_ARGUMENT\n"; + out << "#undef NS_GSD_ENGINE_DIRECT_CONVERT_OBJECT_ARGUMENT\n"; +} + +void writeEngineDirectWrapper(std::ostringstream& out, DispatchKind kind, + const std::string& wrapperName, + const MDSignature* signature) { + if (kind == DispatchKind::BlockInvoke) { + return; + } + + std::string returnType; + if (!mapTypeToCpp(signature->returnType, &returnType, true)) { + return; + } + + std::vector argTypeInfos; + std::vector argTypes; + argTypes.reserve(signature->arguments.size()); + argTypeInfos.reserve(signature->arguments.size()); + for (const auto* arg : signature->arguments) { + std::string argType; + if (!mapTypeToCpp(arg, &argType, false)) { + return; + } + argTypeInfos.push_back(arg); + argTypes.push_back(argType); + } + + out << "static inline bool " << wrapperName + << "(napi_env env, Cif* cif, void* fnptr, "; + if (kind == DispatchKind::ObjCMethod) { + out << "id self, SEL selector, "; + } + out << "const napi_value* argv, void* rvalue) {\n"; + + out << " using Fn = " << returnType << " (*)("; + bool first = true; + if (kind == DispatchKind::ObjCMethod) { + out << "id, SEL"; + first = false; + } + for (const auto& argType : argTypes) { + if (!first) { + out << ", "; + } + out << argType; + first = false; + } + out << ");\n"; + out << " auto fn = reinterpret_cast(fnptr);\n"; + + std::vector cleanupArgIndexes; + cleanupArgIndexes.reserve(argTypes.size()); + for (size_t i = 0; i < argTypes.size(); i++) { + if (argKindMayNeedCleanup(argTypeInfos[i]->kind)) { + cleanupArgIndexes.push_back(i); + } + } + const bool hasCleanupArgs = !cleanupArgIndexes.empty(); + if (hasCleanupArgs) { + out << " bool shouldFreeAny = false;\n"; + } + if (returnType != "void") { + out << " " << returnType << " nativeResult{};\n"; + } + + for (size_t i = 0; i < argTypes.size(); i++) { + out << " " << argTypes[i] << " arg" << i << "{};\n"; + if (argKindMayNeedCleanup(argTypeInfos[i]->kind)) { + out << " bool shouldFree" << i << " = false;\n"; + } + } + + if (hasCleanupArgs) { + out << " auto cleanupManagedArgs = [&]() {\n"; + out << " if (shouldFreeAny) {\n"; + if (kind == DispatchKind::CFunction && returnType != "void") { + out << " void* returnPointerValue = nullptr;\n"; + out << " if (cif->returnType != nullptr && cif->returnType->type == " + "&ffi_type_pointer) {\n"; + out << " returnPointerValue = " + "*reinterpret_cast(&nativeResult);\n"; + out << " }\n"; + } + for (const auto i : cleanupArgIndexes) { + out << " if (shouldFree" << i << ") {\n"; + if (kind == DispatchKind::CFunction && returnType != "void") { + out << " if (returnPointerValue != nullptr && " + "*reinterpret_cast(&arg" + << i << ") == returnPointerValue) {\n"; + out << " } else {\n"; + out << " cif->argTypes[" << i + << "]->free(env, *reinterpret_cast(&arg" << i << "));\n"; + out << " }\n"; + } else { + out << " cif->argTypes[" << i + << "]->free(env, *reinterpret_cast(&arg" << i << "));\n"; + } + out << " }\n"; + } + out << " }\n"; + out << " };\n"; + } + + for (size_t i = 0; i < argTypes.size(); i++) { + writeEngineDirectArgConversion(out, argTypeInfos[i], i); + } + + std::ostringstream callExpr; + callExpr << "fn("; + bool hasAnyCallArg = false; + if (kind == DispatchKind::ObjCMethod) { + callExpr << "self, selector"; + hasAnyCallArg = true; + } + for (size_t i = 0; i < argTypes.size(); i++) { + if (hasAnyCallArg) { + callExpr << ", "; + } + callExpr << "arg" << i; + hasAnyCallArg = true; + } + callExpr << ")"; + + if (returnType == "void") { + out << " " << callExpr.str() << ";\n"; + } else { + out << " nativeResult = " << callExpr.str() << ";\n"; + out << " *reinterpret_cast<" << returnType + << "*>(rvalue) = nativeResult;\n"; + } + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return true;\n"; + out << "}\n\n"; +} + +} // namespace metagen::signature_dispatch diff --git a/metadata-generator/src/SignatureDispatchEmitter/Hermes.cpp b/metadata-generator/src/SignatureDispatchEmitter/Hermes.cpp new file mode 100644 index 00000000..74feb009 --- /dev/null +++ b/metadata-generator/src/SignatureDispatchEmitter/Hermes.cpp @@ -0,0 +1,548 @@ +#include "SignatureDispatchEmitter/Shared.h" + +#include +#include + +namespace metagen::signature_dispatch { + +std::string makeHermesDirectReturnWrapperName(DispatchKind kind, size_t index) { + std::ostringstream stream; + stream << "dh"; + switch (kind) { + case DispatchKind::ObjCMethod: + stream << "o"; + break; + case DispatchKind::CFunction: + stream << "c"; + break; + case DispatchKind::BlockInvoke: + stream << "b"; + break; + } + stream << toBase36(index); + return stream.str(); +} + +std::string makeHermesFrameDirectReturnWrapperName(DispatchKind kind, + size_t index) { + std::ostringstream stream; + stream << "hf"; + switch (kind) { + case DispatchKind::ObjCMethod: + stream << "o"; + break; + case DispatchKind::CFunction: + stream << "c"; + break; + case DispatchKind::BlockInvoke: + stream << "b"; + break; + } + stream << toBase36(index); + return stream.str(); +} + +bool canSetHermesReturnDirectly(MDTypeKind kind) { + switch (kind) { + case mdTypeVoid: + case mdTypeBool: + case mdTypeChar: + case mdTypeUChar: + case mdTypeUInt8: + case mdTypeSShort: + case mdTypeUShort: + case mdTypeSInt: + case mdTypeUInt: + case mdTypeSLong: + case mdTypeULong: + case mdTypeSInt64: + case mdTypeUInt64: + case mdTypeFloat: + case mdTypeDouble: + return true; + default: + return false; + } +} + +bool canSetHermesObjCReturnDirectly(MDTypeKind kind) { + if (canSetHermesReturnDirectly(kind)) { + return true; + } + + switch (kind) { + case mdTypeAnyObject: + case mdTypeProtocolObject: + case mdTypeClassObject: + case mdTypeInstanceObject: + case mdTypeNSStringObject: + case mdTypeNSMutableStringObject: + return true; + default: + return false; + } +} + +void writeHermesDirectReturnValue(std::ostringstream& out, DispatchKind dispatchKind, + MDTypeKind kind, + const std::string& valueExpr) { + // Emits an open failure branch; the caller appends cleanup and `return false`. + switch (kind) { + case mdTypeVoid: + out << " if (!SetHermesGeneratedVoidReturn(env, result)) {\n"; + break; + case mdTypeBool: + out << " if (!SetHermesGeneratedBoolReturn(cif, result, " << valueExpr + << " != 0)) {\n"; + break; + case mdTypeChar: + out << " if (!SetHermesGeneratedInt8Return(cif, result, static_cast(" + << valueExpr << "))) {\n"; + break; + case mdTypeUChar: + case mdTypeUInt8: + out << " if (!SetHermesGeneratedUInt8Return(cif, result, static_cast(" + << valueExpr << "))) {\n"; + break; + case mdTypeSShort: + out << " if (!SetHermesGeneratedInt16Return(cif, result, static_cast(" + << valueExpr << "))) {\n"; + break; + case mdTypeUShort: + out << " if (!SetHermesGeneratedUInt16Return(env, cif, result, " + "static_cast(" + << valueExpr << "))) {\n"; + break; + case mdTypeSInt: + out << " if (!SetHermesGeneratedInt32Return(cif, result, static_cast(" + << valueExpr << "))) {\n"; + break; + case mdTypeUInt: + out << " if (!SetHermesGeneratedUInt32Return(cif, result, static_cast(" + << valueExpr << "))) {\n"; + break; + case mdTypeSLong: + case mdTypeSInt64: + out << " if (!SetHermesGeneratedInt64Return(env, cif, result, " + "static_cast(" + << valueExpr << "))) {\n"; + break; + case mdTypeULong: + case mdTypeUInt64: + out << " if (!SetHermesGeneratedUInt64Return(env, cif, result, " + "static_cast(" + << valueExpr << "))) {\n"; + break; + case mdTypeFloat: + case mdTypeDouble: + out << " if (!SetHermesGeneratedDoubleReturn(cif, result, static_cast(" + << valueExpr << "))) {\n"; + break; + case mdTypeAnyObject: + case mdTypeProtocolObject: + case mdTypeClassObject: + case mdTypeInstanceObject: + case mdTypeNSStringObject: + case mdTypeNSMutableStringObject: + if (dispatchKind == DispatchKind::ObjCMethod) { + out << " if (!TryFastSetHermesGeneratedObjCObjectReturnValue(" + "env, cif, returnContext, selector, cif->returnType->kind, " + << valueExpr << ", result)) {\n"; + } else { + out << " if (!TryFastConvertHermesReturnValue(env, cif, " + "cif->returnType->kind, &" + << valueExpr << ", result)) {\n"; + } + break; + default: + out << " if (!TryFastConvertHermesReturnValue(env, cif, static_cast(" + << static_cast(kind) << "), &" << valueExpr << ", result)) {\n"; + break; + } +} +bool canUseHermesDirectReturnWrapper(DispatchKind kind, + const MDSignature* signature, + HermesDirectReturnCallSite callSite) { + if (callSite == HermesDirectReturnCallSite::FastCallback && + kind == DispatchKind::BlockInvoke) { + return false; + } + + if (signature == nullptr || signature->returnType == nullptr) { + return false; + } + + const bool canSetReturnDirectly = + kind == DispatchKind::ObjCMethod + ? canSetHermesObjCReturnDirectly(signature->returnType->kind) + : canSetHermesReturnDirectly(signature->returnType->kind); + if (!canSetReturnDirectly) { + return false; + } + + for (const auto* arg : signature->arguments) { + if (arg == nullptr || argKindMayNeedCleanup(arg->kind)) { + return false; + } + } + + return true; +} +const char* hermesFrameRawConverterForKind(MDTypeKind kind) { + switch (kind) { + case mdTypeBool: + return "TryFastConvertHermesGeneratedBoolRawArgument"; + case mdTypeChar: + return "TryFastConvertHermesGeneratedInt8RawArgument"; + case mdTypeUChar: + case mdTypeUInt8: + return "TryFastConvertHermesGeneratedUInt8RawArgument"; + case mdTypeSShort: + return "TryFastConvertHermesGeneratedInt16RawArgument"; + case mdTypeUShort: + return "TryFastConvertHermesGeneratedUInt16RawArgument"; + case mdTypeSInt: + return "TryFastConvertHermesGeneratedInt32RawArgument"; + case mdTypeUInt: + return "TryFastConvertHermesGeneratedUInt32RawArgument"; + case mdTypeSLong: + case mdTypeSInt64: + return "TryFastConvertHermesGeneratedInt64RawArgument"; + case mdTypeULong: + case mdTypeUInt64: + return "TryFastConvertHermesGeneratedUInt64RawArgument"; + case mdTypeFloat: + return "TryFastConvertHermesGeneratedFloatRawArgument"; + case mdTypeDouble: + return "TryFastConvertHermesGeneratedDoubleRawArgument"; + default: + return nullptr; + } +} +void writeHermesFrameArgConversion(std::ostringstream& out, + const MDTypeInfo* type, size_t index) { + if (type == nullptr) { + out << " return false;\n"; + return; + } + + if (const char* rawConverter = hermesFrameRawConverterForKind(type->kind)) { + out << " if (!" << rawConverter << "(argRaw" << index << ", &arg" + << index << ")) {\n"; + out << " napi_value argValue" << index + << " = hermesDispatchFrameArg(argsBase, " << index << ");\n"; + out << " bool ignoredShouldFree = false;\n"; + out << " bool ignoredShouldFreeAny = false;\n"; + out << " cif->argTypes[" << index << "]->toNative(env, argValue" + << index << ", &arg" << index + << ", &ignoredShouldFree, &ignoredShouldFreeAny);\n"; + out << " }\n"; + return; + } + + writeEngineDirectArgConversion( + out, type, index, "argValue" + std::to_string(index)); +} +void writeHermesDirectReturnWrapper(std::ostringstream& out, DispatchKind kind, + const std::string& wrapperName, + const MDSignature* signature) { + if (!canUseHermesDirectReturnWrapper( + kind, signature, HermesDirectReturnCallSite::FastCallback)) { + return; + } + + std::string returnType; + if (!mapTypeToCpp(signature->returnType, &returnType, true)) { + return; + } + + std::vector argTypeInfos; + std::vector argTypes; + argTypes.reserve(signature->arguments.size()); + argTypeInfos.reserve(signature->arguments.size()); + for (const auto* arg : signature->arguments) { + std::string argType; + if (!mapTypeToCpp(arg, &argType, false)) { + return; + } + argTypeInfos.push_back(arg); + argTypes.push_back(argType); + } + + out << "static inline bool " << wrapperName + << "(napi_env env, Cif* cif, void* fnptr, "; + if (kind == DispatchKind::ObjCMethod) { + out << "id self, SEL selector, " + "const HermesObjCReturnContext* returnContext, "; + } + out << "const napi_value* argv, napi_value* result) {\n"; + + out << " using Fn = " << returnType << " (*)("; + bool first = true; + if (kind == DispatchKind::ObjCMethod) { + out << "id, SEL"; + first = false; + } + for (const auto& argType : argTypes) { + if (!first) { + out << ", "; + } + out << argType; + first = false; + } + out << ");\n"; + out << " auto fn = reinterpret_cast(fnptr);\n"; + + std::vector cleanupArgIndexes; + cleanupArgIndexes.reserve(argTypes.size()); + for (size_t i = 0; i < argTypes.size(); i++) { + if (argKindMayNeedCleanup(argTypeInfos[i]->kind)) { + cleanupArgIndexes.push_back(i); + } + } + const bool hasCleanupArgs = !cleanupArgIndexes.empty(); + if (hasCleanupArgs) { + out << " bool shouldFreeAny = false;\n"; + } + if (returnType != "void") { + out << " " << returnType << " nativeResult{};\n"; + } + + for (size_t i = 0; i < argTypes.size(); i++) { + out << " " << argTypes[i] << " arg" << i << "{};\n"; + if (argKindMayNeedCleanup(argTypeInfos[i]->kind)) { + out << " bool shouldFree" << i << " = false;\n"; + } + } + + if (hasCleanupArgs) { + out << " auto cleanupManagedArgs = [&]() {\n"; + out << " if (shouldFreeAny) {\n"; + if (kind == DispatchKind::CFunction && returnType != "void") { + out << " void* returnPointerValue = nullptr;\n"; + out << " if (cif->returnType != nullptr && cif->returnType->type == " + "&ffi_type_pointer) {\n"; + out << " returnPointerValue = " + "*reinterpret_cast(&nativeResult);\n"; + out << " }\n"; + } + for (const auto i : cleanupArgIndexes) { + out << " if (shouldFree" << i << ") {\n"; + if (kind == DispatchKind::CFunction && returnType != "void") { + out << " if (returnPointerValue != nullptr && " + "*reinterpret_cast(&arg" + << i << ") == returnPointerValue) {\n"; + out << " } else {\n"; + out << " cif->argTypes[" << i + << "]->free(env, *reinterpret_cast(&arg" << i << "));\n"; + out << " }\n"; + } else { + out << " cif->argTypes[" << i + << "]->free(env, *reinterpret_cast(&arg" << i << "));\n"; + } + out << " }\n"; + } + out << " }\n"; + out << " };\n"; + } + + for (size_t i = 0; i < argTypes.size(); i++) { + writeEngineDirectArgConversion(out, argTypeInfos[i], i, ""); + } + + std::ostringstream callExpr; + callExpr << "fn("; + bool hasAnyCallArg = false; + if (kind == DispatchKind::ObjCMethod) { + callExpr << "self, selector"; + hasAnyCallArg = true; + } + for (size_t i = 0; i < argTypes.size(); i++) { + if (hasAnyCallArg) { + callExpr << ", "; + } + callExpr << "arg" << i; + hasAnyCallArg = true; + } + callExpr << ")"; + + if (returnType == "void") { + out << " " << callExpr.str() << ";\n"; + writeHermesDirectReturnValue(out, kind, signature->returnType->kind, ""); + } else { + out << " nativeResult = " << callExpr.str() << ";\n"; + writeHermesDirectReturnValue(out, kind, signature->returnType->kind, + "nativeResult"); + } + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return false;\n"; + out << " }\n"; + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return true;\n"; + out << "}\n\n"; +} + +void writeHermesFrameDirectReturnWrapper(std::ostringstream& out, + DispatchKind kind, + const std::string& wrapperName, + const MDSignature* signature) { + if (!canUseHermesDirectReturnWrapper(kind, signature, + HermesDirectReturnCallSite::Frame)) { + return; + } + + std::string returnType; + if (!mapTypeToCpp(signature->returnType, &returnType, true)) { + return; + } + + std::vector argTypeInfos; + std::vector argTypes; + argTypes.reserve(signature->arguments.size()); + argTypeInfos.reserve(signature->arguments.size()); + for (const auto* arg : signature->arguments) { + std::string argType; + if (!mapTypeToCpp(arg, &argType, false)) { + return; + } + argTypeInfos.push_back(arg); + argTypes.push_back(argType); + } + + out << "static inline bool " << wrapperName + << "(napi_env env, Cif* cif, void* fnptr, "; + if (kind == DispatchKind::ObjCMethod) { + out << "id self, SEL selector, " + "const HermesObjCReturnContext* returnContext, "; + } else if (kind == DispatchKind::BlockInvoke) { + out << "void* block, "; + } + out << "const uint64_t* argsBase, napi_value* result) {\n"; + + out << " using Fn = " << returnType << " (*)("; + bool first = true; + if (kind == DispatchKind::ObjCMethod) { + out << "id, SEL"; + first = false; + } else if (kind == DispatchKind::BlockInvoke) { + out << "void*"; + first = false; + } + for (const auto& argType : argTypes) { + if (!first) { + out << ", "; + } + out << argType; + first = false; + } + out << ");\n"; + out << " auto fn = reinterpret_cast(fnptr);\n"; + + std::vector cleanupArgIndexes; + cleanupArgIndexes.reserve(argTypes.size()); + for (size_t i = 0; i < argTypes.size(); i++) { + if (argKindMayNeedCleanup(argTypeInfos[i]->kind)) { + cleanupArgIndexes.push_back(i); + } + } + const bool hasCleanupArgs = !cleanupArgIndexes.empty(); + if (hasCleanupArgs) { + out << " bool shouldFreeAny = false;\n"; + } + if (returnType != "void") { + out << " " << returnType << " nativeResult{};\n"; + } + + for (size_t i = 0; i < argTypes.size(); i++) { + if (hermesFrameRawConverterForKind(argTypeInfos[i]->kind) != nullptr) { + out << " uint64_t argRaw" << i + << " = hermesDispatchFrameRawArg(argsBase, " << i << ");\n"; + } else { + out << " napi_value argValue" << i + << " = hermesDispatchFrameArg(argsBase, " << i << ");\n"; + } + out << " " << argTypes[i] << " arg" << i << "{};\n"; + if (argKindMayNeedCleanup(argTypeInfos[i]->kind)) { + out << " bool shouldFree" << i << " = false;\n"; + } + } + + if (hasCleanupArgs) { + out << " auto cleanupManagedArgs = [&]() {\n"; + out << " if (shouldFreeAny) {\n"; + if (kind != DispatchKind::ObjCMethod && returnType != "void") { + out << " void* returnPointerValue = nullptr;\n"; + out << " if (cif->returnType != nullptr && cif->returnType->type == " + "&ffi_type_pointer) {\n"; + out << " returnPointerValue = " + "*reinterpret_cast(&nativeResult);\n"; + out << " }\n"; + } + for (const auto i : cleanupArgIndexes) { + out << " if (shouldFree" << i << ") {\n"; + if (kind != DispatchKind::ObjCMethod && returnType != "void") { + out << " if (returnPointerValue != nullptr && " + "*reinterpret_cast(&arg" + << i << ") == returnPointerValue) {\n"; + out << " } else {\n"; + out << " cif->argTypes[" << i + << "]->free(env, *reinterpret_cast(&arg" << i << "));\n"; + out << " }\n"; + } else { + out << " cif->argTypes[" << i + << "]->free(env, *reinterpret_cast(&arg" << i << "));\n"; + } + out << " }\n"; + } + out << " }\n"; + out << " };\n"; + } + + for (size_t i = 0; i < argTypes.size(); i++) { + writeHermesFrameArgConversion(out, argTypeInfos[i], i); + } + + std::ostringstream callExpr; + callExpr << "fn("; + bool hasAnyCallArg = false; + if (kind == DispatchKind::ObjCMethod) { + callExpr << "self, selector"; + hasAnyCallArg = true; + } else if (kind == DispatchKind::BlockInvoke) { + callExpr << "block"; + hasAnyCallArg = true; + } + for (size_t i = 0; i < argTypes.size(); i++) { + if (hasAnyCallArg) { + callExpr << ", "; + } + callExpr << "arg" << i; + hasAnyCallArg = true; + } + callExpr << ")"; + + if (returnType == "void") { + out << " " << callExpr.str() << ";\n"; + writeHermesDirectReturnValue(out, kind, signature->returnType->kind, ""); + } else { + out << " nativeResult = " << callExpr.str() << ";\n"; + writeHermesDirectReturnValue(out, kind, signature->returnType->kind, + "nativeResult"); + } + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return false;\n"; + out << " }\n"; + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return true;\n"; + out << "}\n\n"; +} + +} // namespace metagen::signature_dispatch diff --git a/metadata-generator/src/SignatureDispatchEmitter/Napi.cpp b/metadata-generator/src/SignatureDispatchEmitter/Napi.cpp new file mode 100644 index 00000000..efcd6a40 --- /dev/null +++ b/metadata-generator/src/SignatureDispatchEmitter/Napi.cpp @@ -0,0 +1,428 @@ +#include "SignatureDispatchEmitter/Shared.h" + +#include +#include + +namespace metagen::signature_dispatch { + +std::string makeNapiWrapperName(DispatchKind kind, size_t index) { + std::ostringstream stream; + stream << "dn"; + switch (kind) { + case DispatchKind::ObjCMethod: + stream << "o"; + break; + case DispatchKind::CFunction: + stream << "c"; + break; + case DispatchKind::BlockInvoke: + stream << "b"; + break; + } + stream << toBase36(index); + return stream.str(); +} +std::string makePreparedWrapperName(DispatchKind kind, size_t index) { + std::ostringstream stream; + stream << "dp"; + switch (kind) { + case DispatchKind::ObjCMethod: + stream << "o"; + break; + case DispatchKind::CFunction: + stream << "c"; + break; + case DispatchKind::BlockInvoke: + stream << "b"; + break; + } + stream << toBase36(index); + return stream.str(); +} +void writeFastNapiArgConversion(std::ostringstream& out, const MDTypeInfo* type, + size_t index, bool hasCleanupArgs) { + const char* failCleanup = hasCleanupArgs ? " cleanupManagedArgs();\n" : ""; + if (type == nullptr) { + out << failCleanup; + out << " return false;\n"; + return; + } + + switch (type->kind) { + case mdTypeChar: { + out << " int32_t tmpArg" << index << " = 0;\n"; + out << " if (napi_get_value_int32(env, argv[" << index << "], &tmpArg" + << index << ") != napi_ok) {\n"; + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return false;\n"; + out << " }\n"; + out << " arg" << index << " = static_cast(tmpArg" << index + << ");\n"; + break; + } + case mdTypeUChar: + case mdTypeUInt8: { + out << " uint32_t tmpArg" << index << " = 0;\n"; + out << " if (napi_get_value_uint32(env, argv[" << index << "], &tmpArg" + << index << ") != napi_ok) {\n"; + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return false;\n"; + out << " }\n"; + out << " arg" << index << " = static_cast(tmpArg" << index + << ");\n"; + break; + } + case mdTypeSShort: { + out << " int32_t tmpArg" << index << " = 0;\n"; + out << " if (napi_get_value_int32(env, argv[" << index << "], &tmpArg" + << index << ") != napi_ok) {\n"; + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return false;\n"; + out << " }\n"; + out << " arg" << index << " = static_cast(tmpArg" << index + << ");\n"; + break; + } + case mdTypeUShort: { + out << " if (!TryFastConvertNapiUInt16Argument(env, argv[" << index + << "], &arg" << index << ")) {\n"; + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return false;\n"; + out << " }\n"; + break; + } + case mdTypeSInt: { + out << " if (napi_get_value_int32(env, argv[" << index << "], &arg" + << index << ") != napi_ok) {\n"; + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return false;\n"; + out << " }\n"; + break; + } + case mdTypeUInt: { + out << " if (napi_get_value_uint32(env, argv[" << index << "], &arg" + << index << ") != napi_ok) {\n"; + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return false;\n"; + out << " }\n"; + break; + } + case mdTypeSLong: + case mdTypeSInt64: { + out << " if (napi_get_value_int64(env, argv[" << index << "], &arg" + << index << ") != napi_ok) {\n"; + out << " bool lossless" << index << " = false;\n"; + out << " if (napi_get_value_bigint_int64(env, argv[" << index + << "], &arg" << index << ", &lossless" << index + << ") != napi_ok) {\n"; + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return false;\n"; + out << " }\n"; + out << " }\n"; + break; + } + case mdTypeULong: + case mdTypeUInt64: { + out << " bool lossless" << index << " = false;\n"; + out << " if (napi_get_value_bigint_uint64(env, argv[" << index + << "], &arg" << index << ", &lossless" << index + << ") != napi_ok) {\n"; + out << " int64_t signedValue" << index << " = 0;\n"; + out << " if (napi_get_value_int64(env, argv[" << index + << "], &signedValue" << index << ") != napi_ok) {\n"; + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return false;\n"; + out << " }\n"; + out << " arg" << index << " = static_cast(signedValue" + << index << ");\n"; + out << " }\n"; + break; + } + case mdTypeFloat: { + out << " double tmpArg" << index << " = 0.0;\n"; + out << " if (napi_get_value_double(env, argv[" << index << "], &tmpArg" + << index << ") != napi_ok) {\n"; + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return false;\n"; + out << " }\n"; + out << " arg" << index << " = static_cast(tmpArg" << index + << ");\n"; + break; + } + case mdTypeDouble: { + out << " if (napi_get_value_double(env, argv[" << index << "], &arg" + << index << ") != napi_ok) {\n"; + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return false;\n"; + out << " }\n"; + out << " if (std::isnan(arg" << index << ") || std::isinf(arg" << index + << ")) {\n"; + out << " arg" << index << " = 0.0;\n"; + out << " }\n"; + break; + } + case mdTypeBool: { + out << " bool boolValue" << index << " = false;\n"; + out << " if (napi_get_value_bool(env, argv[" << index << "], &boolValue" + << index << ") != napi_ok) {\n"; + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return false;\n"; + out << " }\n"; + out << " arg" << index << " = static_cast(boolValue" << index + << " ? 1 : 0);\n"; + break; + } + default: + out << failCleanup; + out << " return false;\n"; + break; + } +} + +void writeNapiWrapper(std::ostringstream& out, DispatchKind kind, + const std::string& wrapperName, + const MDSignature* signature) { + std::string returnType; + if (!mapTypeToCpp(signature->returnType, &returnType, true)) { + return; + } + + std::vector argTypeInfos; + std::vector argTypes; + argTypes.reserve(signature->arguments.size()); + argTypeInfos.reserve(signature->arguments.size()); + for (const auto* arg : signature->arguments) { + std::string argType; + if (!mapTypeToCpp(arg, &argType, false)) { + return; + } + argTypeInfos.push_back(arg); + argTypes.push_back(argType); + } + + out << "static inline bool " << wrapperName + << "(napi_env env, Cif* cif, void* fnptr, "; + if (kind == DispatchKind::ObjCMethod) { + out << "id self, SEL selector, "; + } + out << "const napi_value* argv, void* rvalue) {\n"; + + out << " using Fn = " << returnType << " (*)("; + bool first = true; + if (kind == DispatchKind::ObjCMethod) { + out << "id, SEL"; + first = false; + } + for (const auto& argType : argTypes) { + if (!first) { + out << ", "; + } + out << argType; + first = false; + } + out << ");\n"; + out << " auto fn = reinterpret_cast(fnptr);\n"; + std::vector cleanupArgIndexes; + std::vector noCleanupManagedArgIndexes; + cleanupArgIndexes.reserve(argTypes.size()); + noCleanupManagedArgIndexes.reserve(argTypes.size()); + for (size_t i = 0; i < argTypes.size(); i++) { + if (!isFastDirectNapiKind(argTypeInfos[i]->kind)) { + if (argKindMayNeedCleanup(argTypeInfos[i]->kind)) { + cleanupArgIndexes.push_back(i); + } else { + noCleanupManagedArgIndexes.push_back(i); + } + } + } + const bool hasCleanupArgs = !cleanupArgIndexes.empty(); + if (hasCleanupArgs) { + out << " bool shouldFreeAny = false;\n"; + } + if (!noCleanupManagedArgIndexes.empty()) { + out << " bool ignoredShouldFree = false;\n"; + out << " bool ignoredShouldFreeAny = false;\n"; + } + if (returnType != "void") { + out << " " << returnType << " nativeResult{};\n"; + } + + for (size_t i = 0; i < argTypes.size(); i++) { + out << " " << argTypes[i] << " arg" << i << "{};\n"; + if (!isFastDirectNapiKind(argTypeInfos[i]->kind) && + argKindMayNeedCleanup(argTypeInfos[i]->kind)) { + out << " bool shouldFree" << i << " = false;\n"; + } + } + + if (hasCleanupArgs) { + out << " auto cleanupManagedArgs = [&]() {\n"; + out << " if (shouldFreeAny) {\n"; + if (kind == DispatchKind::CFunction && returnType != "void") { + out << " void* returnPointerValue = nullptr;\n"; + out << " if (cif->returnType != nullptr && cif->returnType->type == " + "&ffi_type_pointer) {\n"; + out << " returnPointerValue = " + "*reinterpret_cast(&nativeResult);\n"; + out << " }\n"; + } + for (const auto i : cleanupArgIndexes) { + out << " if (shouldFree" << i << ") {\n"; + if (kind == DispatchKind::CFunction && returnType != "void") { + out << " if (returnPointerValue != nullptr && " + "*reinterpret_cast(&arg" + << i << ") == returnPointerValue) {\n"; + out << " // Returning an argument pointer keeps ownership " + "with " + "the return value.\n"; + out << " } else {\n"; + out << " cif->argTypes[" << i + << "]->free(env, *reinterpret_cast(&arg" << i << "));\n"; + out << " }\n"; + } else { + out << " cif->argTypes[" << i + << "]->free(env, *reinterpret_cast(&arg" << i << "));\n"; + } + out << " }\n"; + } + out << " }\n"; + out << " };\n"; + } + + for (size_t i = 0; i < argTypes.size(); i++) { + if (isFastDirectNapiKind(argTypeInfos[i]->kind)) { + writeFastNapiArgConversion(out, argTypeInfos[i], i, hasCleanupArgs); + } else if (isFastManagedNapiKind(argTypeInfos[i]->kind)) { + out << " if (!TryFastConvertNapiArgument(env, static_cast(" + << static_cast(argTypeInfos[i]->kind) << "), argv[" << i + << "], &arg" << i << ")) {\n"; + if (argKindMayNeedCleanup(argTypeInfos[i]->kind)) { + out << " cif->argTypes[" << i << "]->toNative(env, argv[" << i + << "], &arg" << i << ", &shouldFree" << i << ", &shouldFreeAny);\n"; + } else { + out << " ignoredShouldFree = false;\n"; + out << " ignoredShouldFreeAny = false;\n"; + out << " cif->argTypes[" << i << "]->toNative(env, argv[" << i + << "], &arg" << i + << ", &ignoredShouldFree, &ignoredShouldFreeAny);\n"; + } + out << " }\n"; + } else { + if (argKindMayNeedCleanup(argTypeInfos[i]->kind)) { + out << " cif->argTypes[" << i << "]->toNative(env, argv[" << i + << "], &arg" << i << ", &shouldFree" << i << ", &shouldFreeAny);\n"; + } else { + out << " ignoredShouldFree = false;\n"; + out << " ignoredShouldFreeAny = false;\n"; + out << " cif->argTypes[" << i << "]->toNative(env, argv[" << i + << "], &arg" << i + << ", &ignoredShouldFree, &ignoredShouldFreeAny);\n"; + } + } + } + + std::ostringstream callExpr; + callExpr << "fn("; + bool hasAnyCallArg = false; + if (kind == DispatchKind::ObjCMethod) { + callExpr << "self, selector"; + hasAnyCallArg = true; + } + for (size_t i = 0; i < argTypes.size(); i++) { + if (hasAnyCallArg) { + callExpr << ", "; + } + callExpr << "arg" << i; + hasAnyCallArg = true; + } + callExpr << ")"; + + if (returnType == "void") { + out << " " << callExpr.str() << ";\n"; + } else { + out << " nativeResult = " << callExpr.str() << ";\n"; + out << " *reinterpret_cast<" << returnType + << "*>(rvalue) = nativeResult;\n"; + } + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + + out << " return true;\n"; + out << "}\n\n"; +} +void writePreparedWrapper(std::ostringstream& out, DispatchKind kind, + const std::string& wrapperName, + const MDSignature* signature) { + if (kind != DispatchKind::BlockInvoke) { + return; + } + + std::string returnType; + if (!mapTypeToCpp(signature->returnType, &returnType, true)) { + return; + } + + std::vector argTypes; + argTypes.reserve(signature->arguments.size()); + for (const auto* arg : signature->arguments) { + std::string argType; + if (!mapTypeToCpp(arg, &argType, false)) { + return; + } + argTypes.push_back(argType); + } + + out << "static inline void " << wrapperName + << "(void* fnptr, void** avalues, void* rvalue) {\n"; + out << " using Fn = " << returnType << " (*)(void*"; + for (const auto& argType : argTypes) { + out << ", " << argType; + } + out << ");\n"; + out << " auto fn = reinterpret_cast(fnptr);\n"; + out << " void* block = *reinterpret_cast(avalues[0]);\n"; + for (size_t i = 0; i < argTypes.size(); i++) { + out << " " << argTypes[i] << " arg" << i << " = *reinterpret_cast<" + << argTypes[i] << "*>(avalues[" << (i + 1) << "]);\n"; + } + + std::ostringstream callExpr; + callExpr << "fn(block"; + for (size_t i = 0; i < argTypes.size(); i++) { + callExpr << ", arg" << i; + } + callExpr << ")"; + + if (returnType == "void") { + out << " " << callExpr.str() << ";\n"; + } else { + out << " *reinterpret_cast<" << returnType + << "*>(rvalue) = " << callExpr.str() << ";\n"; + } + out << "}\n\n"; +} + +} // namespace metagen::signature_dispatch diff --git a/metadata-generator/src/SignatureDispatchEmitter/Shared.cpp b/metadata-generator/src/SignatureDispatchEmitter/Shared.cpp new file mode 100644 index 00000000..86d38637 --- /dev/null +++ b/metadata-generator/src/SignatureDispatchEmitter/Shared.cpp @@ -0,0 +1,552 @@ +#include "SignatureDispatchEmitter/Shared.h" + +#include +#include +#include +#include +#include + +namespace metagen::signature_dispatch { + +constexpr uint64_t kFNV64OffsetBasis = 14695981039346656037ull; +constexpr uint64_t kFNV64Prime = 1099511628211ull; + +uint64_t hashBytesFnv1a(const void* data, size_t size, + uint64_t seed = kFNV64OffsetBasis) { + const auto* bytes = static_cast(data); + uint64_t hash = seed; + for (size_t i = 0; i < size; i++) { + hash ^= static_cast(bytes[i]); + hash *= kFNV64Prime; + } + return hash; +} + +uint64_t composeDispatchId(uint64_t signatureHash, DispatchKind 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); +} + +MDTypeKind canonicalizeSignatureTypeKind(MDTypeKind kind) { + switch (kind) { + case mdTypeAnyObject: + case mdTypeProtocolObject: + case mdTypeClassObject: + case mdTypeInstanceObject: + case mdTypeNSStringObject: + case mdTypeNSMutableStringObject: + return mdTypeAnyObject; + default: + return kind; + } +} + +template +void appendIntegral(uint64_t* hash, std::string* key, T value) { + using Unsigned = typename std::make_unsigned::type; + Unsigned unsignedValue = static_cast(value); + for (size_t i = 0; i < sizeof(Unsigned); i++) { + const uint8_t byte = + static_cast((unsignedValue >> (i * 8)) & 0xFF); + *hash = hashBytesFnv1a(&byte, sizeof(byte), *hash); + if (key != nullptr) { + key->push_back(static_cast(byte)); + } + } +} + +bool appendCanonicalSignature( + const MDSignature* signature, MDSectionOffset signatureOffset, + const SignatureMap& signatures, + std::unordered_set* activeSignatures, uint64_t* hash, + std::string* key); + +bool appendCanonicalType(const MDTypeInfo* type, const SignatureMap& signatures, + std::unordered_set* activeSignatures, + uint64_t* hash, std::string* key) { + if (type == nullptr || hash == nullptr) { + return false; + } + + appendIntegral(hash, key, 0xB0); + const MDTypeKind rawKind = type->kind; + const MDTypeKind canonicalKind = canonicalizeSignatureTypeKind(rawKind); + appendIntegral(hash, key, static_cast(canonicalKind)); + + switch (rawKind) { + case mdTypeArray: + case mdTypeVector: + case mdTypeExtVector: + case mdTypeComplex: + appendIntegral(hash, key, type->arraySize); + if (!appendCanonicalType(type->elementType, signatures, activeSignatures, + hash, key)) { + return false; + } + break; + + case mdTypeStruct: + appendIntegral(hash, key, type->structOffset); + break; + + case mdTypePointer: + if (!appendCanonicalType(type->pointeeType, signatures, activeSignatures, + hash, key)) { + return false; + } + break; + + case mdTypeBlock: + case mdTypeFunctionPointer: { + const MDSectionOffset nestedSignatureOffset = type->signatureOffset; + auto nestedIt = signatures.find(nestedSignatureOffset); + if (nestedSignatureOffset == MD_SECTION_OFFSET_NULL || + nestedIt == signatures.end() || nestedIt->second == nullptr) { + break; + } + + if (!appendCanonicalSignature(nestedIt->second, nestedSignatureOffset, + signatures, activeSignatures, hash, key)) { + return false; + } + break; + } + + default: + break; + } + + appendIntegral(hash, key, 0xBF); + return true; +} + +bool appendCanonicalSignature( + const MDSignature* signature, MDSectionOffset signatureOffset, + const SignatureMap& signatures, + std::unordered_set* activeSignatures, uint64_t* hash, + std::string* key) { + if (signature == nullptr || hash == nullptr || activeSignatures == nullptr) { + return false; + } + + const bool trackRecursion = signatureOffset != MD_SECTION_OFFSET_NULL; + if (trackRecursion) { + if (activeSignatures->find(signatureOffset) != activeSignatures->end()) { + appendIntegral(hash, key, 0xEE); + return true; + } + activeSignatures->insert(signatureOffset); + } + + appendIntegral(hash, key, 0xA0); + appendIntegral(hash, key, signature->isVariadic ? 1 : 0); + + if (!appendCanonicalType(signature->returnType, signatures, activeSignatures, + hash, key)) { + if (trackRecursion) { + activeSignatures->erase(signatureOffset); + } + return false; + } + + uint32_t argCount = 0; + for (const auto* arg : signature->arguments) { + if (!appendCanonicalType(arg, signatures, activeSignatures, hash, key)) { + if (trackRecursion) { + activeSignatures->erase(signatureOffset); + } + return false; + } + argCount++; + } + + appendIntegral(hash, key, argCount); + appendIntegral(hash, key, 0xAF); + + if (trackRecursion) { + activeSignatures->erase(signatureOffset); + } + return true; +} + +uint64_t signatureHash(const MDSignature* signature, + MDSectionOffset signatureOffset, + const SignatureMap& signatures, + std::string* canonicalKeyOut) { + if (signature == nullptr) { + return 0; + } + + uint64_t hash = kFNV64OffsetBasis; + std::unordered_set activeSignatures; + if (!appendCanonicalSignature(signature, signatureOffset, signatures, + &activeSignatures, &hash, canonicalKeyOut)) { + return 0; + } + return hash; +} + +bool mapTypeToCpp(const MDTypeInfo* type, std::string* out, + bool allowVoid = false); + +bool mapPointerPointeeToCpp(const MDTypeInfo* type, std::string* out) { + if (type == nullptr || out == nullptr) { + return false; + } + + switch (type->kind) { + case mdTypeVoid: + *out = "void"; + return true; + case mdTypePointer: { + std::string nested; + if (!mapPointerPointeeToCpp(type->pointeeType, &nested)) { + return false; + } + *out = nested + "*"; + return true; + } + default: + return mapTypeToCpp(type, out, false); + } +} + +bool mapTypeToCpp(const MDTypeInfo* type, std::string* out, bool allowVoid) { + if (type == nullptr || out == nullptr) { + return false; + } + + switch (type->kind) { + case mdTypeVoid: + if (!allowVoid) { + return false; + } + *out = "void"; + return true; + + case mdTypeBool: + *out = "uint8_t"; + return true; + + case mdTypeChar: + *out = "int8_t"; + return true; + + case mdTypeUChar: + case mdTypeUInt8: + *out = "uint8_t"; + return true; + + case mdTypeSShort: + *out = "int16_t"; + return true; + + case mdTypeUShort: + *out = "uint16_t"; + return true; + + case mdTypeSInt: + *out = "int32_t"; + return true; + + case mdTypeUInt: + *out = "uint32_t"; + return true; + + case mdTypeSLong: + case mdTypeSInt64: + *out = "int64_t"; + return true; + + case mdTypeULong: + case mdTypeUInt64: + *out = "uint64_t"; + return true; + + case mdTypeFloat: + *out = "float"; + return true; + + case mdTypeDouble: + *out = "double"; + return true; + + case mdTypeString: + *out = "char*"; + return true; + + case mdTypeAnyObject: + case mdTypeProtocolObject: + case mdTypeClassObject: + case mdTypeInstanceObject: + case mdTypeNSStringObject: + case mdTypeNSMutableStringObject: + *out = "id"; + return true; + + case mdTypeClass: + *out = "Class"; + return true; + + case mdTypeSelector: + *out = "SEL"; + return true; + + case mdTypePointer: { + std::string pointee; + if (!mapPointerPointeeToCpp(type->pointeeType, &pointee)) { + return false; + } + *out = pointee + "*"; + return true; + } + + case mdTypeOpaquePointer: + case mdTypeBlock: + case mdTypeFunctionPointer: + *out = "void*"; + return true; + + default: + return false; + } +} + +bool isSignatureSupported(const MDSignature* signature) { + if (signature == nullptr || signature->isVariadic) { + return false; + } + // Keep generated dispatch focused on hot paths and avoid huge wrappers. + if (signature->arguments.size() > 8) { + return false; + } + + std::string unused; + if (!mapTypeToCpp(signature->returnType, &unused, true)) { + return false; + } + + for (const auto* arg : signature->arguments) { + if (!mapTypeToCpp(arg, &unused, false)) { + return false; + } + } + + return true; +} + +std::string toHexLiteral(uint64_t value) { + std::ostringstream stream; + stream << "0x" << std::hex << std::setw(16) << std::setfill('0') << value + << "ULL"; + return stream.str(); +} + +std::string toBase36(size_t value) { + std::ostringstream stream; + if (value == 0) { + stream << '0'; + return stream.str(); + } + + std::string digits; + while (value > 0) { + const size_t digit = value % 36; + digits.push_back( + static_cast(digit < 10 ? ('0' + digit) : ('a' + digit - 10))); + value /= 36; + } + std::reverse(digits.begin(), digits.end()); + stream << digits; + return stream.str(); +} + +bool isFastDirectNapiKind(MDTypeKind kind) { + switch (kind) { + case mdTypeBool: + case mdTypeChar: + case mdTypeUChar: + case mdTypeUInt8: + case mdTypeSShort: + case mdTypeUShort: + case mdTypeSInt: + case mdTypeUInt: + case mdTypeSLong: + case mdTypeULong: + case mdTypeSInt64: + case mdTypeUInt64: + case mdTypeFloat: + case mdTypeDouble: + return true; + default: + return false; + } +} + +bool isFastManagedNapiKind(MDTypeKind kind) { + switch (kind) { + case mdTypeAnyObject: + case mdTypeProtocolObject: + case mdTypeClassObject: + case mdTypeInstanceObject: + case mdTypeNSStringObject: + case mdTypeNSMutableStringObject: + case mdTypeSelector: + return true; + default: + return false; + } +} + +bool argKindMayNeedCleanup(MDTypeKind kind) { + switch (kind) { + case mdTypeAnyObject: + case mdTypeProtocolObject: + case mdTypeClassObject: + case mdTypeInstanceObject: + case mdTypeNSStringObject: + case mdTypeNSMutableStringObject: + case mdTypeClass: + case mdTypeSelector: + return false; + default: + return !isFastDirectNapiKind(kind); + } +} + +std::string makeWrapperShapeKey(DispatchKind kind, + const MDSignature* signature) { + if (signature == nullptr) { + return {}; + } + + std::string returnType; + if (!mapTypeToCpp(signature->returnType, &returnType, true)) { + return {}; + } + + std::ostringstream key; + key << static_cast(kind) << "|" << returnType << "|"; + for (const auto* arg : signature->arguments) { + std::string argType; + if (!mapTypeToCpp(arg, &argType, false)) { + return {}; + } + + if (isFastDirectNapiKind(arg->kind)) { + key << "F" << static_cast(arg->kind); + } else if (isFastManagedNapiKind(arg->kind)) { + key << "H" << static_cast(arg->kind); + } else { + key << "M" << argType; + } + key << "|"; + } + + return key.str(); +} +void collectBlockUsesFromSignature(MDSectionOffset signatureOffset, + const SignatureMap& signatures, + std::unordered_set* active, + std::vector* uses); + +void collectBlockUsesFromType(const MDTypeInfo* type, + const SignatureMap& signatures, + std::unordered_set* active, + std::vector* uses) { + if (type == nullptr || active == nullptr || uses == nullptr) { + return; + } + + switch (type->kind) { + case mdTypeArray: + case mdTypeVector: + case mdTypeExtVector: + case mdTypeComplex: + collectBlockUsesFromType(type->elementType, signatures, active, uses); + break; + + case mdTypePointer: + collectBlockUsesFromType(type->pointeeType, signatures, active, uses); + break; + + case mdTypeBlock: + if (type->signatureOffset != MD_SECTION_OFFSET_NULL) { + uses->push_back({DispatchKind::BlockInvoke, type->signatureOffset, 0}); + collectBlockUsesFromSignature(type->signatureOffset, signatures, active, + uses); + } + break; + + case mdTypeFunctionPointer: + if (type->signatureOffset != MD_SECTION_OFFSET_NULL) { + collectBlockUsesFromSignature(type->signatureOffset, signatures, active, + uses); + } + break; + + default: + break; + } +} + +void collectBlockUsesFromSignature(MDSectionOffset signatureOffset, + const SignatureMap& signatures, + std::unordered_set* active, + std::vector* uses) { + if (active == nullptr || uses == nullptr || + signatureOffset == MD_SECTION_OFFSET_NULL || + active->find(signatureOffset) != active->end()) { + return; + } + + auto it = signatures.find(signatureOffset); + if (it == signatures.end() || it->second == nullptr) { + return; + } + + active->insert(signatureOffset); + const MDSignature* signature = it->second; + collectBlockUsesFromType(signature->returnType, signatures, active, uses); + for (const auto* arg : signature->arguments) { + collectBlockUsesFromType(arg, signatures, active, uses); + } + active->erase(signatureOffset); +} + +void collectMethodUses(const std::vector& members, + std::vector* uses) { + if (uses == nullptr) { + return; + } + + for (auto* member : members) { + if (member == nullptr) { + continue; + } + + const uint8_t methodFlags = + (member->flags & mdMemberReturnOwned) != 0 ? 1 : 0; + + if ((member->flags & mdMemberProperty) != 0) { + if (member->getterSignature != MD_SECTION_OFFSET_NULL) { + uses->push_back( + {DispatchKind::ObjCMethod, member->getterSignature, methodFlags}); + } + if (((member->flags & mdMemberReadonly) == 0) && + member->setterSignature != MD_SECTION_OFFSET_NULL) { + uses->push_back({DispatchKind::ObjCMethod, member->setterSignature, 0}); + } + } else { + if (member->signature != MD_SECTION_OFFSET_NULL) { + uses->push_back( + {DispatchKind::ObjCMethod, member->signature, methodFlags}); + } + } + } +} + +} // namespace metagen::signature_dispatch diff --git a/metadata-generator/src/SignatureDispatchEmitter/Shared.h b/metadata-generator/src/SignatureDispatchEmitter/Shared.h new file mode 100644 index 00000000..873723d7 --- /dev/null +++ b/metadata-generator/src/SignatureDispatchEmitter/Shared.h @@ -0,0 +1,96 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "MetadataWriter.h" + +namespace metagen::signature_dispatch { + +enum class DispatchKind : uint8_t { + ObjCMethod = 1, + CFunction = 2, + BlockInvoke = 3, +}; + +struct SignatureUse { + DispatchKind kind; + MDSectionOffset signatureOffset; + uint8_t flags; +}; + +enum class HermesDirectReturnCallSite { + FastCallback, + Frame, +}; + +using SignatureMap = std::unordered_map; + +uint64_t composeDispatchId(uint64_t signatureHash, DispatchKind kind, + uint8_t flags); +uint64_t signatureHash(const MDSignature* signature, + MDSectionOffset signatureOffset, + const SignatureMap& signatures, + std::string* canonicalKeyOut); +bool mapTypeToCpp(const MDTypeInfo* type, std::string* out, bool allowVoid); +bool isSignatureSupported(const MDSignature* signature); +bool isFastDirectNapiKind(MDTypeKind kind); +bool isFastManagedNapiKind(MDTypeKind kind); +bool argKindMayNeedCleanup(MDTypeKind kind); +std::string toHexLiteral(uint64_t value); +std::string toBase36(size_t value); +std::string makeWrapperShapeKey(DispatchKind kind, + const MDSignature* signature); +void collectBlockUsesFromSignature(MDSectionOffset signatureOffset, + const SignatureMap& signatures, + std::unordered_set* active, + std::vector* uses); +void collectMethodUses(const std::vector& members, + std::vector* uses); + +std::string makeNapiWrapperName(DispatchKind kind, size_t index); +std::string makePreparedWrapperName(DispatchKind kind, size_t index); +void writeNapiWrapper(std::ostringstream& out, DispatchKind kind, + const std::string& wrapperName, + const MDSignature* signature); +void writePreparedWrapper(std::ostringstream& out, DispatchKind kind, + const std::string& wrapperName, + const MDSignature* signature); + +std::string makeEngineDirectWrapperName(DispatchKind kind, size_t index); +void writeEngineDirectArgConversion(std::ostringstream& out, + const MDTypeInfo* type, size_t index, + const std::string& valueExpr); +void writeEngineDirectConverterMacros(std::ostringstream& out); +void writeHermesEngineDirectConverterMacros(std::ostringstream& out); +void writeEngineDirectConverterUndefs(std::ostringstream& out); +void writeEngineDirectWrapper(std::ostringstream& out, DispatchKind kind, + const std::string& wrapperName, + const MDSignature* signature); + +std::string makeHermesDirectReturnWrapperName(DispatchKind kind, size_t index); +std::string makeHermesFrameDirectReturnWrapperName(DispatchKind kind, + size_t index); +bool canUseHermesDirectReturnWrapper(DispatchKind kind, + const MDSignature* signature, + HermesDirectReturnCallSite callSite); +void writeHermesDirectReturnWrapper(std::ostringstream& out, DispatchKind kind, + const std::string& wrapperName, + const MDSignature* signature); +void writeHermesFrameDirectReturnWrapper(std::ostringstream& out, + DispatchKind kind, + const std::string& wrapperName, + const MDSignature* signature); + +std::string makeV8WrapperName(DispatchKind kind, size_t index); +void writeV8Wrapper(std::ostringstream& out, DispatchKind kind, + const std::string& wrapperName, + const MDSignature* signature); + +} // namespace metagen::signature_dispatch diff --git a/metadata-generator/src/SignatureDispatchEmitter/V8.cpp b/metadata-generator/src/SignatureDispatchEmitter/V8.cpp new file mode 100644 index 00000000..544cd6c6 --- /dev/null +++ b/metadata-generator/src/SignatureDispatchEmitter/V8.cpp @@ -0,0 +1,500 @@ +#include "SignatureDispatchEmitter/Shared.h" + +#include +#include + +namespace metagen::signature_dispatch { + +std::string makeV8WrapperName(DispatchKind kind, size_t index) { + std::ostringstream stream; + stream << "dv"; + switch (kind) { + case DispatchKind::ObjCMethod: + stream << "o"; + break; + case DispatchKind::CFunction: + stream << "c"; + break; + case DispatchKind::BlockInvoke: + stream << "b"; + break; + } + stream << toBase36(index); + return stream.str(); +} + +bool fastV8ArgConversionNeedsContext(MDTypeKind kind) { + switch (kind) { + case mdTypeChar: + case mdTypeUChar: + case mdTypeUInt8: + case mdTypeSShort: + case mdTypeSInt: + case mdTypeUInt: + case mdTypeSLong: + case mdTypeULong: + case mdTypeSInt64: + case mdTypeUInt64: + case mdTypeFloat: + case mdTypeDouble: + return true; + default: + return false; + } +} + +bool canSetV8ReturnDirectly(MDTypeKind kind) { + switch (kind) { + case mdTypeVoid: + case mdTypeBool: + case mdTypeChar: + case mdTypeUChar: + case mdTypeUInt8: + case mdTypeSShort: + case mdTypeUShort: + case mdTypeSInt: + case mdTypeUInt: + case mdTypeSLong: + case mdTypeULong: + case mdTypeSInt64: + case mdTypeUInt64: + case mdTypeFloat: + case mdTypeDouble: + return true; + default: + return false; + } +} + +bool canTrySetV8ObjectReturnDirectly(MDTypeKind kind) { + switch (kind) { + case mdTypeAnyObject: + case mdTypeProtocolObject: + case mdTypeClassObject: + case mdTypeInstanceObject: + case mdTypeNSStringObject: + case mdTypeNSMutableStringObject: + return true; + default: + return false; + } +} + +void writeV8DirectReturnValue(std::ostringstream& out, MDTypeKind kind, + const std::string& valueExpr) { + switch (kind) { + case mdTypeBool: + out << " info.GetReturnValue().Set(" << valueExpr << " != 0);\n"; + break; + case mdTypeChar: + case mdTypeSShort: + case mdTypeSInt: + out << " info.GetReturnValue().Set(static_cast(" << valueExpr + << "));\n"; + break; + case mdTypeUChar: + case mdTypeUInt8: + case mdTypeUInt: + out << " info.GetReturnValue().Set(static_cast(" << valueExpr + << "));\n"; + break; + case mdTypeUShort: + out << " setV8DispatchUInt16ReturnValue(info.GetIsolate(), info, " + << "static_cast(" << valueExpr << "));\n"; + break; + case mdTypeSLong: + case mdTypeSInt64: + out << " setV8DispatchInt64ReturnValue(info.GetIsolate(), info, " + << valueExpr << ");\n"; + break; + case mdTypeULong: + case mdTypeUInt64: + out << " setV8DispatchUInt64ReturnValue(info.GetIsolate(), info, " + << valueExpr << ");\n"; + break; + case mdTypeFloat: + case mdTypeDouble: + out << " info.GetReturnValue().Set(static_cast(" << valueExpr + << "));\n"; + break; + default: + break; + } +} + +void writeFastV8ArgConversion(std::ostringstream& out, const MDTypeInfo* type, + size_t index, bool hasCleanupArgs) { + const char* failCleanup = hasCleanupArgs ? " cleanupManagedArgs();\n" : ""; + if (type == nullptr) { + out << failCleanup; + out << " return false;\n"; + return; + } + + switch (type->kind) { + case mdTypeChar: { + out << " int32_t tmpArg" << index << " = 0;\n"; + out << " if (!info[" << index << "]->Int32Value(context).To(&tmpArg" + << index << ")) {\n"; + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return false;\n"; + out << " }\n"; + out << " arg" << index << " = static_cast(tmpArg" << index + << ");\n"; + break; + } + case mdTypeUChar: + case mdTypeUInt8: { + out << " uint32_t tmpArg" << index << " = 0;\n"; + out << " if (!info[" << index << "]->Uint32Value(context).To(&tmpArg" + << index << ")) {\n"; + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return false;\n"; + out << " }\n"; + out << " arg" << index << " = static_cast(tmpArg" << index + << ");\n"; + break; + } + case mdTypeSShort: { + out << " int32_t tmpArg" << index << " = 0;\n"; + out << " if (!info[" << index << "]->Int32Value(context).To(&tmpArg" + << index << ")) {\n"; + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return false;\n"; + out << " }\n"; + out << " arg" << index << " = static_cast(tmpArg" << index + << ");\n"; + break; + } + case mdTypeUShort: { + out << " if (!TryFastConvertV8UInt16Argument(env, info[" << index + << "], &arg" << index << ")) {\n"; + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return false;\n"; + out << " }\n"; + break; + } + case mdTypeSInt: { + out << " if (!info[" << index << "]->Int32Value(context).To(&arg" + << index << ")) {\n"; + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return false;\n"; + out << " }\n"; + break; + } + case mdTypeUInt: { + out << " if (!info[" << index << "]->Uint32Value(context).To(&arg" + << index << ")) {\n"; + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return false;\n"; + out << " }\n"; + break; + } + case mdTypeSLong: + case mdTypeSInt64: { + out << " if (info[" << index << "]->IsBigInt()) {\n"; + out << " bool lossless" << index << " = false;\n"; + out << " arg" << index << " = info[" << index + << "].As()->Int64Value(&lossless" << index << ");\n"; + out << " } else if (!info[" << index + << "]->IntegerValue(context).To(&arg" << index << ")) {\n"; + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return false;\n"; + out << " }\n"; + break; + } + case mdTypeULong: + case mdTypeUInt64: { + out << " if (info[" << index << "]->IsBigInt()) {\n"; + out << " bool lossless" << index << " = false;\n"; + out << " arg" << index << " = info[" << index + << "].As()->Uint64Value(&lossless" << index << ");\n"; + out << " } else {\n"; + out << " int64_t signedValue" << index << " = 0;\n"; + out << " if (!info[" << index + << "]->IntegerValue(context).To(&signedValue" << index << ")) {\n"; + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return false;\n"; + out << " }\n"; + out << " arg" << index << " = static_cast(signedValue" + << index << ");\n"; + out << " }\n"; + break; + } + case mdTypeFloat: { + out << " double tmpArg" << index << " = 0.0;\n"; + out << " if (!info[" << index << "]->NumberValue(context).To(&tmpArg" + << index << ")) {\n"; + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return false;\n"; + out << " }\n"; + out << " arg" << index << " = static_cast(tmpArg" << index + << ");\n"; + break; + } + case mdTypeDouble: { + out << " if (!info[" << index << "]->NumberValue(context).To(&arg" + << index << ")) {\n"; + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return false;\n"; + out << " }\n"; + out << " if (std::isnan(arg" << index << ") || std::isinf(arg" << index + << ")) {\n"; + out << " arg" << index << " = 0.0;\n"; + out << " }\n"; + break; + } + case mdTypeBool: { + out << " if (!info[" << index << "]->IsBoolean()) {\n"; + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + out << " return false;\n"; + out << " }\n"; + out << " arg" << index << " = static_cast(info[" << index + << "]->BooleanValue(info.GetIsolate()) ? 1 : 0);\n"; + break; + } + default: + out << failCleanup; + out << " return false;\n"; + break; + } +} +void writeV8Wrapper(std::ostringstream& out, DispatchKind kind, + const std::string& wrapperName, + const MDSignature* signature) { + if (kind == DispatchKind::BlockInvoke) { + return; + } + + std::string returnType; + if (!mapTypeToCpp(signature->returnType, &returnType, true)) { + return; + } + + std::vector argTypeInfos; + std::vector argTypes; + argTypes.reserve(signature->arguments.size()); + argTypeInfos.reserve(signature->arguments.size()); + for (const auto* arg : signature->arguments) { + std::string argType; + if (!mapTypeToCpp(arg, &argType, false)) { + return; + } + argTypeInfos.push_back(arg); + argTypes.push_back(argType); + } + + out << "static inline bool " << wrapperName + << "(napi_env env, Cif* cif, void* fnptr, "; + if (kind == DispatchKind::ObjCMethod) { + out << "id self, SEL selector, void* bridgeState, bool returnOwned, " + "bool receiverIsClass, bool propertyAccess, "; + } + out << "const v8::FunctionCallbackInfo& info, void* rvalue, " + "bool* didSetReturnValue) {\n"; + if (!argTypes.empty()) { + out << " if (info.Length() < " << argTypes.size() << ") {\n"; + out << " return false;\n"; + out << " }\n"; + } + bool needsContext = false; + for (const auto* arg : argTypeInfos) { + if (arg != nullptr && fastV8ArgConversionNeedsContext(arg->kind)) { + needsContext = true; + break; + } + } + if (needsContext) { + out << " v8::Local context = info.GetIsolate()->GetCurrentContext();\n"; + } + + out << " using Fn = " << returnType << " (*)("; + bool first = true; + if (kind == DispatchKind::ObjCMethod) { + out << "id, SEL"; + first = false; + } + for (const auto& argType : argTypes) { + if (!first) { + out << ", "; + } + out << argType; + first = false; + } + out << ");\n"; + out << " auto fn = reinterpret_cast(fnptr);\n"; + + std::vector cleanupArgIndexes; + std::vector noCleanupManagedArgIndexes; + cleanupArgIndexes.reserve(argTypes.size()); + noCleanupManagedArgIndexes.reserve(argTypes.size()); + for (size_t i = 0; i < argTypes.size(); i++) { + if (!isFastDirectNapiKind(argTypeInfos[i]->kind)) { + if (argKindMayNeedCleanup(argTypeInfos[i]->kind)) { + cleanupArgIndexes.push_back(i); + } else { + noCleanupManagedArgIndexes.push_back(i); + } + } + } + const bool hasCleanupArgs = !cleanupArgIndexes.empty(); + const bool setsReturnDirectly = + canSetV8ReturnDirectly(signature->returnType->kind); + const bool triesObjectReturnDirectly = + kind == DispatchKind::ObjCMethod && + canTrySetV8ObjectReturnDirectly(signature->returnType->kind); + if (hasCleanupArgs) { + out << " bool shouldFreeAny = false;\n"; + } + if (!noCleanupManagedArgIndexes.empty()) { + out << " bool ignoredShouldFree = false;\n"; + out << " bool ignoredShouldFreeAny = false;\n"; + } + if (returnType != "void") { + out << " " << returnType << " nativeResult{};\n"; + } + + for (size_t i = 0; i < argTypes.size(); i++) { + out << " " << argTypes[i] << " arg" << i << "{};\n"; + if (!isFastDirectNapiKind(argTypeInfos[i]->kind) && + argKindMayNeedCleanup(argTypeInfos[i]->kind)) { + out << " bool shouldFree" << i << " = false;\n"; + } + } + + if (hasCleanupArgs) { + out << " auto cleanupManagedArgs = [&]() {\n"; + out << " if (shouldFreeAny) {\n"; + if (kind == DispatchKind::CFunction && returnType != "void") { + out << " void* returnPointerValue = nullptr;\n"; + out << " if (cif->returnType != nullptr && cif->returnType->type == " + "&ffi_type_pointer) {\n"; + out << " returnPointerValue = " + "*reinterpret_cast(&nativeResult);\n"; + out << " }\n"; + } + for (const auto i : cleanupArgIndexes) { + out << " if (shouldFree" << i << ") {\n"; + if (kind == DispatchKind::CFunction && returnType != "void") { + out << " if (returnPointerValue != nullptr && " + "*reinterpret_cast(&arg" + << i << ") == returnPointerValue) {\n"; + out << " } else {\n"; + out << " cif->argTypes[" << i + << "]->free(env, *reinterpret_cast(&arg" << i << "));\n"; + out << " }\n"; + } else { + out << " cif->argTypes[" << i + << "]->free(env, *reinterpret_cast(&arg" << i << "));\n"; + } + out << " }\n"; + } + out << " }\n"; + out << " };\n"; + } + + for (size_t i = 0; i < argTypes.size(); i++) { + if (isFastDirectNapiKind(argTypeInfos[i]->kind)) { + writeFastV8ArgConversion(out, argTypeInfos[i], i, hasCleanupArgs); + } else if (isFastManagedNapiKind(argTypeInfos[i]->kind)) { + out << " if (!TryFastConvertV8Argument(env, static_cast(" + << static_cast(argTypeInfos[i]->kind) << "), info[" << i + << "], &arg" << i << ")) {\n"; + if (argKindMayNeedCleanup(argTypeInfos[i]->kind)) { + out << " cif->argTypes[" << i + << "]->toNative(env, v8LocalValueToNapiValue(info[" << i + << "]), &arg" << i << ", &shouldFree" << i + << ", &shouldFreeAny);\n"; + } else { + out << " ignoredShouldFree = false;\n"; + out << " ignoredShouldFreeAny = false;\n"; + out << " cif->argTypes[" << i + << "]->toNative(env, v8LocalValueToNapiValue(info[" << i + << "]), &arg" << i + << ", &ignoredShouldFree, &ignoredShouldFreeAny);\n"; + } + out << " }\n"; + } else { + if (argKindMayNeedCleanup(argTypeInfos[i]->kind)) { + out << " cif->argTypes[" << i + << "]->toNative(env, v8LocalValueToNapiValue(info[" << i + << "]), &arg" << i << ", &shouldFree" << i + << ", &shouldFreeAny);\n"; + } else { + out << " ignoredShouldFree = false;\n"; + out << " ignoredShouldFreeAny = false;\n"; + out << " cif->argTypes[" << i + << "]->toNative(env, v8LocalValueToNapiValue(info[" << i + << "]), &arg" << i + << ", &ignoredShouldFree, &ignoredShouldFreeAny);\n"; + } + } + } + + std::ostringstream callExpr; + callExpr << "fn("; + bool hasAnyCallArg = false; + if (kind == DispatchKind::ObjCMethod) { + callExpr << "self, selector"; + hasAnyCallArg = true; + } + for (size_t i = 0; i < argTypes.size(); i++) { + if (hasAnyCallArg) { + callExpr << ", "; + } + callExpr << "arg" << i; + hasAnyCallArg = true; + } + callExpr << ")"; + + if (returnType == "void") { + out << " " << callExpr.str() << ";\n"; + out << " *didSetReturnValue = true;\n"; + } else if (setsReturnDirectly) { + out << " nativeResult = " << callExpr.str() << ";\n"; + writeV8DirectReturnValue(out, signature->returnType->kind, "nativeResult"); + out << " *didSetReturnValue = true;\n"; + } else if (triesObjectReturnDirectly) { + out << " nativeResult = " << callExpr.str() << ";\n"; + out << " *reinterpret_cast<" << returnType + << "*>(rvalue) = nativeResult;\n"; + out << " if (TryFastSetV8GeneratedObjCObjectReturnValue(env, info, cif, bridgeState, self, " + "selector, nativeResult, returnOwned, receiverIsClass, propertyAccess)) {\n"; + out << " *didSetReturnValue = true;\n"; + out << " }\n"; + } else { + out << " nativeResult = " << callExpr.str() << ";\n"; + out << " *reinterpret_cast<" << returnType + << "*>(rvalue) = nativeResult;\n"; + } + if (hasCleanupArgs) { + out << " cleanupManagedArgs();\n"; + } + + out << " return true;\n"; + out << "}\n\n"; +} + +} // namespace metagen::signature_dispatch diff --git a/package.json b/package.json index f0d5032b..a43a2970 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,11 @@ "build-vision": "./scripts/build_all_vision.sh", "build-visionos": "./scripts/build_all_vision.sh", "build-node-api": "./scripts/build_all_react_native.sh", + "build-rn-turbomodule": "./scripts/build_react_native_turbomodule.sh", + "check:ffi-boundaries": "./scripts/check_ffi_boundaries.sh", + "test-rn-turbomodule": "./scripts/test_react_native_turbomodule.sh", + "test-rn-ffi": "./scripts/test_react_native_ffi_compat.sh", + "demo-rn-turbomodule": "./scripts/create_react_native_demo.sh", "pack:ios": "./scripts/build_npm_ios.sh", "pack:macos": "./scripts/build_npm_macos.sh", "pack:visionos": "./scripts/build_npm_vision.sh", @@ -48,7 +53,8 @@ "build:macos-napi": "./scripts/build_nativescript.sh --no-iphone --no-simulator --macos-napi", "nsr": "./dist/nsr", "test:macos": "node ./scripts/run-tests-macos.js ./build/test-results/macos-junit.xml", - "test:ios": "node ./scripts/run-tests-ios.js" + "test:ios": "node ./scripts/run-tests-ios.js", + "benchmark:objc-dispatch": "node ./benchmarks/objc-dispatch/run.js" }, "license": "Apache-2.0", "devDependencies": { diff --git a/packages/ios/package.json b/packages/ios/package.json index 767acf5e..f96f7163 100644 --- a/packages/ios/package.json +++ b/packages/ios/package.json @@ -1,6 +1,6 @@ { "name": "@nativescript/ios", - "version": "9.0.0-napi-v8.2", + "version": "0.0.1", "description": "NativeScript Runtime for iOS", "keywords": [ "NativeScript", diff --git a/packages/react-native/LICENSE b/packages/react-native/LICENSE new file mode 100644 index 00000000..6f231e7c --- /dev/null +++ b/packages/react-native/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2023 Yagiz Nizipli and Daniel Lemire + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/packages/react-native/NativeScriptNativeApi.podspec b/packages/react-native/NativeScriptNativeApi.podspec new file mode 100644 index 00000000..d65543a7 --- /dev/null +++ b/packages/react-native/NativeScriptNativeApi.podspec @@ -0,0 +1,54 @@ +require "json" + +package = JSON.parse(File.read(File.join(__dir__, "package.json"))) +fabric_enabled = ENV["RCT_NEW_ARCH_ENABLED"] == "1" + +folly_compiler_flags = "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -DFOLLY_CFG_NO_COROUTINES=1 -Wno-comma -Wno-shorten-64-to-32" + +Pod::Spec.new do |s| + s.name = "NativeScriptNativeApi" + s.version = package["version"] + s.summary = package["description"] + s.homepage = "https://github.com/NativeScript/napi-ios" + s.license = "Apache-2.0" + s.author = package["author"] + s.platforms = { :ios => "13.0" } + s.source = { :git => "https://github.com/NativeScript/napi-ios.git", :tag => "react-native-v#{s.version}" } + s.requires_arc = false + + s.source_files = [ + "ios/**/*.{h,mm}", + "native-api-jsi/**/*.{h,mm}" + ] + s.exclude_files = "ios/Fabric/**/*" unless fabric_enabled + s.public_header_files = "ios/**/*.h" + s.resource_bundles = { + "NativeScriptNativeApi" => ["metadata/*.nsmd"] + } + s.vendored_frameworks = "ios/vendor/Libffi.xcframework" + + s.compiler_flags = folly_compiler_flags + s.pod_target_xcconfig = { + "CLANG_CXX_LANGUAGE_STANDARD" => "c++20", + "CLANG_CXX_LIBRARY" => "libc++", + "GCC_PREPROCESSOR_DEFINITIONS" => "$(inherited) TARGET_ENGINE_HERMES=1", + "HEADER_SEARCH_PATHS" => [ + "\"$(PODS_TARGET_SRCROOT)/ios\"", + "\"$(PODS_TARGET_SRCROOT)/native-api-jsi\"", + "\"$(PODS_TARGET_SRCROOT)/native-api-jsi/metadata/include\"", + "\"$(PODS_TARGET_SRCROOT)/ios/vendor/libffi/include\"", + "\"$(PODS_ROOT)/Headers/Public/React-Codegen\"", + "\"$(PODS_ROOT)/Headers/Private/React-Codegen\"", + "\"$(PODS_ROOT)/Headers/Public/ReactCommon\"", + "\"$(PODS_ROOT)/Headers/Private/ReactCommon\"" + ].join(" ") + } + + if respond_to?(:install_modules_dependencies, true) + install_modules_dependencies(s) + else + s.dependency "React-Core" + s.dependency "React-jsi" + s.dependency "ReactCommon/turbomodule/core" + end +end diff --git a/packages/react-native/README.md b/packages/react-native/README.md new file mode 100644 index 00000000..bb88da7c --- /dev/null +++ b/packages/react-native/README.md @@ -0,0 +1,190 @@ +# @nativescript/react-native + +React Native TurboModule wrapper for the NativeScript Native API JSI bridge on +Hermes. + +The module exposes one small TurboModule whose `init()` method attaches the +NativeScript Native API host object to `globalThis.__nativeScriptNativeApi` and +installs lazy NativeScript-style globals for classes and C functions. The host +object itself is pure JSI and is shared with the NativeScript Hermes runtime. + +```ts +import NativeScript from "@nativescript/react-native"; + +NativeScript.init(); + +const object = NSObject.new(); +``` + +For UIKit work that must happen on the main thread, pass a callback to the JSI +host object's `runOnUI()` helper. The callback itself stays on React Native's JS +thread; NativeScript native calls made inside the callback are synchronously +performed on UIKit's main thread. + +```ts +await NativeScript.runOnUI(() => { + UIApplication.sharedApplication.keyWindow.tintColor = UIColor.systemPinkColor; +}); +``` + +## Defining native UIKit views in JS + +Use `defineUIKitView()` to turn a NativeScript-created `UIView` tree into a +normal React Native component. The package owns the RN host view; your +definition owns the UIKit subtree. `create`, `update`, `mounted`, and `dispose` +run through the NativeScript UI dispatcher, so UIKit calls are safe and use the +same globals and iOS SDK types as NativeScript. + +```tsx +import NativeScript, {defineUIKitView} from "@nativescript/react-native"; +import type {UIKitViewRef} from "@nativescript/react-native"; + +NativeScript.init(); + +type BadgeProps = { + title: string; + tone?: "blue" | "green"; +}; + +export const NativeBadge = defineUIKitView({ + displayName: "NativeBadge", + create() { + const view = UIView.alloc().initWithFrame(CGRectZero); + const label = UILabel.alloc().initWithFrame(CGRectZero); + label.tag = 1; + label.textAlignment = NSTextAlignment.Center; + label.textColor = UIColor.whiteColor; + label.autoresizingMask = + UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight; + view.addSubview(label); + return view; + }, + update(view, props) { + view.backgroundColor = + props.tone === "green" ? UIColor.systemGreenColor : UIColor.systemBlueColor; + view.layer.cornerRadius = 12; + view.clipsToBounds = true; + const label = view.viewWithTag(1) as UILabel; + label.text = props.title; + }, +}); + +; +``` + +Forward a ref when you need imperative access: + +```tsx +const badgeRef = useRef>(null); + +await badgeRef.current?.runOnUI((view) => { + view.alpha = 0.8; +}); +``` + +React Native view props such as `style`, `testID`, accessibility props, responder +props, and `pointerEvents` go to the host component. Your own props go to the +UIKit definition; use `nativeProps(props)` when a plugin prop should also affect +the RN host. + +The published package includes generated NativeScript metadata, the libffi +xcframework, and generated iOS SDK TypeScript declarations. Build it from the +repository root with: + +```sh +npm run build-rn-turbomodule +``` + +The tarball is written to `packages/react-native/dist/` and copied to +`build/npm-tarballs/`. + +To verify it inside a generated React Native iOS app: + +```sh +npm run test-rn-turbomodule +``` + +## Using the package in a React Native app + +1. Build or download the package tarball. +2. Install it in an RN app that has Hermes and the New Architecture enabled: + + ```sh + npm install /path/to/nativescript-react-native-0.0.1.tgz + cd ios + RCT_NEW_ARCH_ENABLED=1 USE_HERMES=1 pod install + ``` + +3. Initialize it before using native APIs: + + ```ts + import NativeScript from "@nativescript/react-native"; + + NativeScript.init(); + + await NativeScript.runOnUI(() => { + UIApplication.sharedApplication.keyWindow.tintColor = + UIColor.systemPinkColor; + }); + ``` + +## Using the package in an Expo app + +Expo Go cannot load this package because it contains custom native code. Use an +Expo development build, EAS Build, or `npx expo run:ios`. + +1. Install the package: + + ```sh + npx expo install @nativescript/react-native + ``` + + When testing a local tarball: + + ```sh + npm install /path/to/nativescript-react-native-0.0.1.tgz + ``` + +2. Add the config plugin to `app.json` or `app.config.js`: + + ```json + { + "expo": { + "plugins": ["@nativescript/react-native"] + } + } + ``` + + The plugin configures iOS for Hermes and the React Native New Architecture, + which are required by this JSI TurboModule. + +3. Prebuild and run the iOS development build: + + ```sh + npx expo prebuild --platform ios + npx expo run:ios + ``` + +4. Initialize NativeScript in app code before using native APIs: + + ```tsx + import NativeScript, {defineUIKitView} from "@nativescript/react-native"; + + NativeScript.init(); + + const NativeBadge = defineUIKitView<{title: string}, UIView>({ + create() { + const view = UIView.alloc().initWithFrame(CGRectZero); + const label = UILabel.alloc().initWithFrame(CGRectZero); + label.tag = 1; + label.textAlignment = NSTextAlignment.Center; + view.addSubview(label); + return view; + }, + update(view, props) { + view.backgroundColor = UIColor.systemBlueColor; + const label = view.viewWithTag(1) as UILabel; + label.text = props.title; + }, + }); + ``` diff --git a/packages/react-native/app.plugin.js b/packages/react-native/app.plugin.js new file mode 100644 index 00000000..9247126c --- /dev/null +++ b/packages/react-native/app.plugin.js @@ -0,0 +1 @@ +module.exports = require('./plugin/withNativeScriptReactNative'); diff --git a/packages/react-native/ios/Fabric/NativeScriptUIViewComponentView.h b/packages/react-native/ios/Fabric/NativeScriptUIViewComponentView.h new file mode 100644 index 00000000..c6567eb5 --- /dev/null +++ b/packages/react-native/ios/Fabric/NativeScriptUIViewComponentView.h @@ -0,0 +1,4 @@ +#import + +@interface NativeScriptUIViewComponentView : RCTViewComponentView +@end diff --git a/packages/react-native/ios/Fabric/NativeScriptUIViewComponentView.mm b/packages/react-native/ios/Fabric/NativeScriptUIViewComponentView.mm new file mode 100644 index 00000000..fea3fb76 --- /dev/null +++ b/packages/react-native/ios/Fabric/NativeScriptUIViewComponentView.mm @@ -0,0 +1,63 @@ +#import "NativeScriptUIViewComponentView.h" + +#import +#import +#import + +#import "NativeScriptUIView.h" + +using namespace facebook::react; + +@implementation NativeScriptUIViewComponentView { + NativeScriptUIView* _containerView; +} + +- (instancetype)initWithFrame:(CGRect)frame { + if (self = [super initWithFrame:frame]) { + static const auto defaultProps = std::make_shared(); + _props = defaultProps; + + _containerView = [[NativeScriptUIView alloc] initWithFrame:self.bounds]; + _containerView.autoresizingMask = + UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + self.contentView = _containerView; + } + + return self; +} + +- (void)dealloc { + [_containerView release]; + [super dealloc]; +} + +- (void)updateProps:(Props::Shared const&)props oldProps:(Props::Shared const&)oldProps { + const auto oldViewProps = std::static_pointer_cast(_props); + const auto newViewProps = std::static_pointer_cast(props); + const std::string oldNativeViewHandle = oldViewProps->nativeViewHandle; + const std::string newNativeViewHandle = newViewProps->nativeViewHandle; + + [super updateProps:props oldProps:oldProps]; + + if (oldNativeViewHandle != newNativeViewHandle) { + NSString* nativeViewHandle = newNativeViewHandle.empty() + ? nil + : [NSString stringWithUTF8String:newNativeViewHandle.c_str()]; + _containerView.nativeViewHandle = nativeViewHandle; + } +} + +- (void)prepareForRecycle { + [super prepareForRecycle]; + _containerView.nativeViewHandle = nil; +} + ++ (ComponentDescriptorProvider)componentDescriptorProvider { + return concreteComponentDescriptorProvider(); +} + +@end + +Class NativeScriptUIViewCls(void) { + return NativeScriptUIViewComponentView.class; +} diff --git a/packages/react-native/ios/NativeScriptNativeApiModule.h b/packages/react-native/ios/NativeScriptNativeApiModule.h new file mode 100644 index 00000000..540613fa --- /dev/null +++ b/packages/react-native/ios/NativeScriptNativeApiModule.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include + +#include +#include +#include + +namespace facebook::react { + +class NativeScriptNativeApiModule + : public NativeScriptNativeApiCxxSpec { + public: + explicit NativeScriptNativeApiModule(std::shared_ptr jsInvoker); + + bool install(jsi::Runtime& runtime, std::string metadataPath); + bool isInstalled(jsi::Runtime& runtime); + std::string defaultMetadataPath(jsi::Runtime& runtime); + std::string getRuntimeBackend(jsi::Runtime& runtime); + bool __writeTestMarker(jsi::Runtime& runtime, std::string content); + + private: + std::shared_ptr jsInvoker_; +}; + +} // namespace facebook::react diff --git a/packages/react-native/ios/NativeScriptNativeApiModule.mm b/packages/react-native/ios/NativeScriptNativeApiModule.mm new file mode 100644 index 00000000..5b379780 --- /dev/null +++ b/packages/react-native/ios/NativeScriptNativeApiModule.mm @@ -0,0 +1,133 @@ +#include "NativeScriptNativeApiModule.h" + +#import +#import + +#include + +#include "NativeApiJsiReactNative.h" + +namespace { + +std::string pathForResource(NSBundle* bundle, NSString* name, NSString* type) { + if (bundle == nil) { + return ""; + } + NSString* path = [bundle pathForResource:name ofType:type]; + return path != nil ? path.UTF8String : ""; +} + +std::string bundledMetadataPath() { +#if TARGET_OS_SIMULATOR +#if defined(__x86_64__) + NSString* metadataName = @"metadata.ios-sim.x86_64"; +#else + NSString* metadataName = @"metadata.ios-sim.arm64"; +#endif +#else + NSString* metadataName = @"metadata.ios.arm64"; +#endif + + std::string path = pathForResource([NSBundle mainBundle], metadataName, @"nsmd"); + if (!path.empty()) { + return path; + } + + Class providerClass = NSClassFromString(@"NativeScriptNativeApiModuleProvider"); + NSBundle* providerBundle = + providerClass != Nil ? [NSBundle bundleForClass:providerClass] : nil; + NSString* resourceBundlePath = + [providerBundle pathForResource:@"NativeScriptNativeApi" ofType:@"bundle"]; + NSBundle* resourceBundle = + resourceBundlePath != nil ? [NSBundle bundleWithPath:resourceBundlePath] : nil; + + path = pathForResource(resourceBundle, metadataName, @"nsmd"); + if (!path.empty()) { + return path; + } + + return pathForResource(resourceBundle, @"metadata", @"nsmd"); +} + +void writeSmokeMarkerIfRequested(const char* stage) { + const char* enabled = getenv("NATIVESCRIPT_RN_TURBO_SMOKE_MARKER"); + if (enabled == nullptr || enabled[0] == '\0') { + return; + } + + NSString* path = [NSTemporaryDirectory() + stringByAppendingPathComponent:@"NativeScriptNativeApiSmoke.marker"]; + NSString* content = + [NSString stringWithFormat:@"stage=%s\n", stage != nullptr ? stage : ""]; + [content writeToFile:path atomically:YES encoding:NSUTF8StringEncoding error:nil]; +} + +bool writeSmokeMarkerContentIfRequested(const std::string& content) { + const char* enabled = getenv("NATIVESCRIPT_RN_TURBO_SMOKE_MARKER"); + if (enabled == nullptr || enabled[0] == '\0') { + return false; + } + + NSString* path = [NSTemporaryDirectory() + stringByAppendingPathComponent:@"NativeScriptNativeApiSmoke.marker"]; + NSString* nativeContent = + [[NSString alloc] initWithBytes:content.data() + length:content.size() + encoding:NSUTF8StringEncoding]; + if (nativeContent == nil) { + nativeContent = @""; + } + + BOOL ok = [nativeContent writeToFile:path + atomically:YES + encoding:NSUTF8StringEncoding + error:nil]; +#if !__has_feature(objc_arc) + [nativeContent release]; +#endif + return ok == YES; +} + +} // namespace + +namespace facebook::react { + +NativeScriptNativeApiModule::NativeScriptNativeApiModule( + std::shared_ptr jsInvoker) + : NativeScriptNativeApiCxxSpec(jsInvoker), jsInvoker_(std::move(jsInvoker)) {} + +bool NativeScriptNativeApiModule::install(jsi::Runtime& runtime, + std::string metadataPath) { + writeSmokeMarkerIfRequested("install:resolve-metadata"); + std::string resolvedMetadataPath = + metadataPath.empty() ? bundledMetadataPath() : metadataPath; + const char* metadataPathArg = + resolvedMetadataPath.empty() ? nullptr : resolvedMetadataPath.c_str(); + + writeSmokeMarkerIfRequested("install:before-jsi"); + auto config = nativescript::MakeReactNativeNativeApiJsiConfig( + jsInvoker_, nullptr, metadataPathArg); + nativescript::InstallNativeApiJSI(runtime, config); + writeSmokeMarkerIfRequested("install:after-jsi"); + return isInstalled(runtime); +} + +bool NativeScriptNativeApiModule::isInstalled(jsi::Runtime& runtime) { + return runtime.global().hasProperty(runtime, "__nativeScriptNativeApi"); +} + +std::string NativeScriptNativeApiModule::defaultMetadataPath(jsi::Runtime&) { + return bundledMetadataPath(); +} + +std::string NativeScriptNativeApiModule::getRuntimeBackend(jsi::Runtime&) { + writeSmokeMarkerIfRequested("getRuntimeBackend"); + return "hermes-jsi"; +} + +bool NativeScriptNativeApiModule::__writeTestMarker(jsi::Runtime&, + std::string content) { + return writeSmokeMarkerContentIfRequested(content); +} + +} // namespace facebook::react diff --git a/packages/react-native/ios/NativeScriptNativeApiModuleProvider.h b/packages/react-native/ios/NativeScriptNativeApiModuleProvider.h new file mode 100644 index 00000000..ea58c873 --- /dev/null +++ b/packages/react-native/ios/NativeScriptNativeApiModuleProvider.h @@ -0,0 +1,9 @@ +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NativeScriptNativeApiModuleProvider : NSObject +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/react-native/ios/NativeScriptNativeApiModuleProvider.mm b/packages/react-native/ios/NativeScriptNativeApiModuleProvider.mm new file mode 100644 index 00000000..997cc91f --- /dev/null +++ b/packages/react-native/ios/NativeScriptNativeApiModuleProvider.mm @@ -0,0 +1,16 @@ +#import "NativeScriptNativeApiModuleProvider.h" + +#import +#import + +#include "NativeScriptNativeApiModule.h" + +@implementation NativeScriptNativeApiModuleProvider + +- (std::shared_ptr)getTurboModule: + (const facebook::react::ObjCTurboModule::InitParams&)params { + return std::make_shared( + params.jsInvoker); +} + +@end diff --git a/packages/react-native/ios/NativeScriptUIView.h b/packages/react-native/ios/NativeScriptUIView.h new file mode 100644 index 00000000..82d09405 --- /dev/null +++ b/packages/react-native/ios/NativeScriptUIView.h @@ -0,0 +1,7 @@ +#import + +@interface NativeScriptUIView : UIView + +@property(nonatomic, copy) NSString* nativeViewHandle; + +@end diff --git a/packages/react-native/ios/NativeScriptUIView.mm b/packages/react-native/ios/NativeScriptUIView.mm new file mode 100644 index 00000000..ae0430c8 --- /dev/null +++ b/packages/react-native/ios/NativeScriptUIView.mm @@ -0,0 +1,75 @@ +#import "NativeScriptUIView.h" + +static UIView* NativeScriptUIViewFromHandle(NSString* handle) { + if (handle == nil || handle.length == 0) { + return nil; + } + + const char* text = handle.UTF8String; + if (text == nullptr || text[0] == '\0') { + return nil; + } + + char* end = nullptr; + unsigned long long address = strtoull(text, &end, 0); + if (address == 0 || end == text || (end != nullptr && *end != '\0')) { + return nil; + } + + id object = reinterpret_cast(static_cast(address)); + if (object == nil || ![object isKindOfClass:UIView.class]) { + return nil; + } + + return static_cast(object); +} + +@implementation NativeScriptUIView { + UIView* _nativeView; +} + +- (void)dealloc { + [_nativeView removeFromSuperview]; + [_nativeView release]; + [_nativeViewHandle release]; + [super dealloc]; +} + +- (void)setNativeViewHandle:(NSString*)nativeViewHandle { + if ((_nativeViewHandle == nativeViewHandle) || + [_nativeViewHandle isEqualToString:nativeViewHandle]) { + return; + } + + [_nativeViewHandle release]; + _nativeViewHandle = [nativeViewHandle copy]; + [self setNativeView:NativeScriptUIViewFromHandle(_nativeViewHandle)]; +} + +- (void)setNativeView:(UIView*)nativeView { + if (_nativeView == nativeView) { + return; + } + + [_nativeView removeFromSuperview]; + [_nativeView release]; + _nativeView = nil; + + if (nativeView == nil) { + return; + } + + _nativeView = [nativeView retain]; + [_nativeView removeFromSuperview]; + _nativeView.frame = self.bounds; + _nativeView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [self addSubview:_nativeView]; + [self setNeedsLayout]; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + _nativeView.frame = self.bounds; +} + +@end diff --git a/packages/react-native/ios/NativeScriptUIViewManager.mm b/packages/react-native/ios/NativeScriptUIViewManager.mm new file mode 100644 index 00000000..ed7af13d --- /dev/null +++ b/packages/react-native/ios/NativeScriptUIViewManager.mm @@ -0,0 +1,18 @@ +#import + +#import "NativeScriptUIView.h" + +@interface NativeScriptUIViewManager : RCTViewManager +@end + +@implementation NativeScriptUIViewManager + +RCT_EXPORT_MODULE(NativeScriptUIView) + +- (UIView*)view { + return [[[NativeScriptUIView alloc] initWithFrame:CGRectZero] autorelease]; +} + +RCT_EXPORT_VIEW_PROPERTY(nativeViewHandle, NSString) + +@end diff --git a/packages/react-native/package.json b/packages/react-native/package.json new file mode 100644 index 00000000..4ba07227 --- /dev/null +++ b/packages/react-native/package.json @@ -0,0 +1,63 @@ +{ + "name": "@nativescript/react-native", + "version": "0.0.1", + "description": "React Native TurboModule for NativeScript Native API access", + "keywords": [ + "NativeScript", + "React Native", + "TurboModule", + "JSI", + "iOS" + ], + "repository": { + "type": "git", + "url": "https://github.com/NativeScript/napi-ios", + "directory": "packages/react-native" + }, + "author": { + "name": "NativeScript Team", + "email": "oss@nativescript.org" + }, + "license": "Apache-2.0", + "main": "src/index.ts", + "react-native": "src/index.ts", + "types": "src/index.d.ts", + "files": [ + "app.plugin.js", + "plugin", + "src", + "types", + "ios", + "metadata", + "native-api-jsi", + "NativeScriptNativeApi.podspec", + "README.md", + "LICENSE" + ], + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": ">=0.79" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + } + }, + "codegenConfig": { + "name": "NativeScriptNativeApiSpec", + "type": "all", + "jsSrcsDir": "src", + "android": { + "javaPackageName": "org.nativescript.nativeapi" + }, + "ios": { + "componentProvider": { + "NativeScriptUIView": "NativeScriptUIViewComponentView" + }, + "modulesProvider": { + "NativeScriptNativeApi": "NativeScriptNativeApiModuleProvider" + } + } + } +} diff --git a/packages/react-native/plugin/withNativeScriptReactNative.js b/packages/react-native/plugin/withNativeScriptReactNative.js new file mode 100644 index 00000000..24f1a5cc --- /dev/null +++ b/packages/react-native/plugin/withNativeScriptReactNative.js @@ -0,0 +1,52 @@ +const pkg = require('../package.json'); + +const DEFAULTS = { + ios: { + hermes: true, + newArchitecture: true, + }, +}; + +function readBoolean(value, fallback) { + return typeof value === 'boolean' ? value : fallback; +} + +function normalizeOptions(options = {}) { + const ios = options.ios || {}; + return { + ios: { + hermes: readBoolean( + ios.hermes, + readBoolean(options.hermes, DEFAULTS.ios.hermes), + ), + newArchitecture: readBoolean( + ios.newArchitecture, + readBoolean(options.newArchitecture, DEFAULTS.ios.newArchitecture), + ), + }, + }; +} + +function withNativeScriptReactNative(config, options) { + const normalized = normalizeOptions(options); + + config.ios = config.ios || {}; + + if (normalized.ios.hermes) { + config.ios.jsEngine = 'hermes'; + } + + if (normalized.ios.newArchitecture) { + // Expo SDKs have accepted both the root and iOS-scoped keys over time. + // Set both so CNG and existing native projects agree on the required RN mode. + config.newArchEnabled = true; + config.ios.newArchEnabled = true; + } + + return config; +} + +module.exports = withNativeScriptReactNative; +module.exports.default = withNativeScriptReactNative; +module.exports.withNativeScriptReactNative = withNativeScriptReactNative; +module.exports.pkg = pkg; diff --git a/packages/react-native/react-native.config.js b/packages/react-native/react-native.config.js new file mode 100644 index 00000000..22b2e13d --- /dev/null +++ b/packages/react-native/react-native.config.js @@ -0,0 +1,7 @@ +module.exports = { + dependency: { + platforms: { + android: null, + }, + }, +}; diff --git a/packages/react-native/src/NativeScriptNativeApi.ts b/packages/react-native/src/NativeScriptNativeApi.ts new file mode 100644 index 00000000..96c9b268 --- /dev/null +++ b/packages/react-native/src/NativeScriptNativeApi.ts @@ -0,0 +1,12 @@ +import type {TurboModule} from 'react-native'; +import {TurboModuleRegistry} from 'react-native'; + +export interface Spec extends TurboModule { + readonly install: (metadataPath: string) => boolean; + readonly isInstalled: () => boolean; + readonly defaultMetadataPath: () => string; + readonly getRuntimeBackend: () => string; + readonly __writeTestMarker: (content: string) => boolean; +} + +export default TurboModuleRegistry.getEnforcing('NativeScriptNativeApi'); diff --git a/packages/react-native/src/NativeScriptUIViewNativeComponent.ts b/packages/react-native/src/NativeScriptUIViewNativeComponent.ts new file mode 100644 index 00000000..281e0607 --- /dev/null +++ b/packages/react-native/src/NativeScriptUIViewNativeComponent.ts @@ -0,0 +1,10 @@ +import type {HostComponent, ViewProps} from 'react-native'; +import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; + +export interface NativeProps extends ViewProps { + nativeViewHandle?: string; +} + +export default codegenNativeComponent( + 'NativeScriptUIView', +) as HostComponent; diff --git a/packages/react-native/src/index.d.ts b/packages/react-native/src/index.d.ts new file mode 100644 index 00000000..7d24ded4 --- /dev/null +++ b/packages/react-native/src/index.d.ts @@ -0,0 +1,87 @@ +/// + +import type { + ForwardRefExoticComponent, + PropsWithoutRef, + RefAttributes, +} from 'react'; +import type {ViewProps} from 'react-native'; + +export type NativeApiHost = { + runtime?: string; + backend?: string; + metadata?: { + classes?: number; + functions?: number; + constants?: number; + protocols?: number; + enums?: number; + structs?: number; + unions?: number; + classNames?: () => string[]; + functionNames?: () => string[]; + constantNames?: () => string[]; + protocolNames?: () => string[]; + enumNames?: () => string[]; + structNames?: () => string[]; + unionNames?: () => string[]; + }; + getProtocol?: (name: string) => unknown; + getStruct?: (name: string) => unknown; + getUnion?: (name: string) => unknown; + runOnUI?: (callback?: () => void) => Promise; + [name: string]: unknown; +}; + +export type InstallOptions = { + globals?: boolean; +}; + +export type UIKitViewDefinition = { + displayName?: string; + create: (props: Readonly) => NativeView; + update?: ( + view: NativeView, + props: Readonly, + previousProps?: Readonly, + ) => void; + mounted?: (view: NativeView, props: Readonly) => void; + dispose?: (view: NativeView, props: Readonly) => void; + nativeProps?: ( + props: Readonly, + ) => Partial | undefined; +}; + +export type UIKitViewRef = { + readonly nativeView: NativeView | null; + runOnUI: (callback: (view: NativeView) => void) => Promise; +}; + +export type UIKitViewComponent = + ForwardRefExoticComponent< + PropsWithoutRef & RefAttributes> + >; + +export function init(metadataPath?: string, options?: InstallOptions): boolean; +export const install: typeof init; +export function installGlobals(): boolean; +export function isInstalled(): boolean; +export function defaultMetadataPath(): string; +export function getRuntimeBackend(): string; +export function runOnUI(callback?: () => void): Promise; +export function defineUIKitView( + definition: UIKitViewDefinition, +): UIKitViewComponent; + +declare const NativeScript: { + init: typeof init; + install: typeof install; + installGlobals: typeof installGlobals; + isInstalled: typeof isInstalled; + defaultMetadataPath: typeof defaultMetadataPath; + defineUIKitView: typeof defineUIKitView; + getRuntimeBackend: typeof getRuntimeBackend; + runOnUI: typeof runOnUI; +}; + +export default NativeScript; diff --git a/packages/react-native/src/index.ts b/packages/react-native/src/index.ts new file mode 100644 index 00000000..2e1aa774 --- /dev/null +++ b/packages/react-native/src/index.ts @@ -0,0 +1,827 @@ +import React, { + forwardRef, + useEffect, + useImperativeHandle, + useRef, + useState, +} from 'react'; +import type { + ForwardRefExoticComponent, + PropsWithoutRef, + RefAttributes, +} from 'react'; +import type {ViewProps} from 'react-native'; +import NativeScriptNativeApi from './NativeScriptNativeApi'; +import NativeScriptUIViewNativeComponent from './NativeScriptUIViewNativeComponent'; + +type NativeApiHost = { + metadata?: { + classNames?: () => string[]; + functionNames?: () => string[]; + constantNames?: () => string[]; + protocolNames?: () => string[]; + enumNames?: () => string[]; + structNames?: () => string[]; + unionNames?: () => string[]; + }; + getProtocol?: (name: string) => unknown; + getStruct?: (name: string) => unknown; + getUnion?: (name: string) => unknown; + runOnUI?: (callback?: () => void) => Promise; + [name: string]: unknown; +}; + +export type InstallOptions = { + globals?: boolean; +}; + +export type UIKitViewDefinition = { + displayName?: string; + create: (props: Readonly) => NativeView; + update?: ( + view: NativeView, + props: Readonly, + previousProps?: Readonly, + ) => void; + mounted?: (view: NativeView, props: Readonly) => void; + dispose?: (view: NativeView, props: Readonly) => void; + nativeProps?: ( + props: Readonly, + ) => Partial | undefined; +}; + +export type UIKitViewRef = { + readonly nativeView: NativeView | null; + runOnUI: (callback: (view: NativeView) => void) => Promise; +}; + +export type UIKitViewComponent = + ForwardRefExoticComponent< + PropsWithoutRef & RefAttributes> + >; + +const nativeApiGlobalName = '__nativeScriptNativeApi'; +const nativeApiGlobalCacheName = '__nativeScriptNativeApiGlobalCache'; +const nativeApiTypeCodeKey = '__nativeApiTypeCode'; +const nativeClassWrappers = new WeakMap(); + +function nativeApiHost(): NativeApiHost | undefined { + return (globalThis as Record)[nativeApiGlobalName] as + | NativeApiHost + | undefined; +} + +function requireNativeApiHost(): NativeApiHost { + const api = nativeApiHost(); + if (!api) { + throw new Error('NativeScript Native API JSI host object was not installed'); + } + return api; +} + +function nativeApiGlobalCache(): Record { + const globalObject = globalThis as Record; + const existing = globalObject[nativeApiGlobalCacheName]; + if (existing && typeof existing === 'object') { + return existing as Record; + } + + const cache: Record = Object.create(null); + Object.defineProperty(globalThis, nativeApiGlobalCacheName, { + configurable: false, + enumerable: false, + writable: false, + value: cache, + }); + return cache; +} + +function cacheNativeGlobal(name: string, value: unknown): void { + if (!name || value === undefined) { + return; + } + nativeApiGlobalCache()[name] = value; +} + +const hostViewPropNames = new Set([ + 'accessible', + 'accessibilityActions', + 'accessibilityElementsHidden', + 'accessibilityHint', + 'accessibilityIgnoresInvertColors', + 'accessibilityLabel', + 'accessibilityLanguage', + 'accessibilityLiveRegion', + 'accessibilityRole', + 'accessibilityState', + 'accessibilityValue', + 'accessibilityViewIsModal', + 'children', + 'collapsable', + 'focusable', + 'hitSlop', + 'id', + 'importantForAccessibility', + 'nativeID', + 'needsOffscreenAlphaCompositing', + 'onAccessibilityAction', + 'onAccessibilityEscape', + 'onAccessibilityTap', + 'onLayout', + 'onMagicTap', + 'onMoveShouldSetResponder', + 'onMoveShouldSetResponderCapture', + 'onResponderEnd', + 'onResponderGrant', + 'onResponderMove', + 'onResponderReject', + 'onResponderRelease', + 'onResponderStart', + 'onResponderTerminate', + 'onResponderTerminationRequest', + 'onStartShouldSetResponder', + 'onStartShouldSetResponderCapture', + 'pointerEvents', + 'removeClippedSubviews', + 'renderToHardwareTextureAndroid', + 'shouldRasterizeIOS', + 'style', + 'testID', +]); + +function splitUIKitViewProps( + props: Props & ViewProps, + definition: UIKitViewDefinition, +): { + nativeProps: ViewProps; + pluginProps: Props & ViewProps; +} { + const nativeProps: Record = {}; + const pluginProps: Record = {}; + + for (const [key, value] of Object.entries(props)) { + if ( + hostViewPropNames.has(key) || + key.startsWith('accessibility') || + key.startsWith('aria-') + ) { + nativeProps[key] = value; + } else { + pluginProps[key] = value; + } + } + + Object.assign(nativeProps, definition.nativeProps?.(props)); + + return { + nativeProps: nativeProps as ViewProps, + pluginProps: pluginProps as Props & ViewProps, + }; +} + +function nativeHandleForUIKitView(view: unknown): string { + const interop = (globalThis as Record).interop; + if (!interop || typeof interop.handleof !== 'function') { + throw new Error('NativeScript interop globals are not installed'); + } + + const pointer = interop.handleof(view); + if (!pointer) { + throw new Error('UIKit view definition returned a value without a native handle'); + } + + if (typeof pointer.toHexString === 'function') { + const text = pointer.toHexString(); + if (typeof text === 'string' && text.length > 0) { + return text; + } + } + + if (typeof pointer.address === 'string' && pointer.address.length > 0) { + return pointer.address; + } + + if (typeof pointer.address === 'number') { + return String(pointer.address); + } + + if (typeof pointer.toNumber === 'function') { + return String(pointer.toNumber()); + } + + throw new Error('UIKit view native handle could not be read'); +} + +function ensureNativeScriptInstalled(): void { + if (!isInstalled()) { + init(); + } +} + +function defineLazyNativeGlobal( + name: string, + resolve: (name: string) => unknown, + force = false, +) { + if (!name) { + return; + } + + if (!force && Object.prototype.hasOwnProperty.call(globalThis, name)) { + try { + cacheNativeGlobal(name, (globalThis as Record)[name]); + } catch { + // Some host globals throw when read; leave those uncached. + } + return; + } + + try { + Object.defineProperty(globalThis, name, { + configurable: true, + enumerable: false, + get() { + const value = resolve(name); + cacheNativeGlobal(name, value); + Object.defineProperty(globalThis, name, { + configurable: true, + enumerable: false, + writable: false, + value, + }); + return value; + }, + }); + } catch { + const value = resolve(name); + if (value !== undefined) { + cacheNativeGlobal(name, value); + Object.defineProperty(globalThis, name, { + configurable: true, + enumerable: false, + writable: false, + value, + }); + } + } +} + +function wrapAggregateConstructor(nativeConstructor: unknown): unknown { + if (typeof nativeConstructor !== 'function') { + return nativeConstructor; + } + + const aggregate = function NativeScriptAggregate(initialValue?: unknown) { + return nativeConstructor(initialValue); + }; + + try { + const hasInstance = Symbol.hasInstance; + Object.defineProperty(aggregate, hasInstance, { + configurable: true, + enumerable: false, + value(value: unknown) { + if (!value || typeof value !== 'object') { + return false; + } + const actual = value as Record; + return ( + actual.kind === (nativeConstructor as Record).kind && + actual.name === (nativeConstructor as Record).runtimeName + ); + }, + }); + } catch { + // Older runtimes can expose Symbol.hasInstance as read-only. + } + + for (const key of [ + 'kind', + 'runtimeName', + 'metadataOffset', + 'sizeof', + 'fields', + 'equals', + ]) { + try { + Object.defineProperty(aggregate, key, { + configurable: true, + enumerable: false, + writable: false, + value: (nativeConstructor as Record)[key], + }); + } catch { + // Best effort metadata copy for runtimes with stricter function objects. + } + } + + return aggregate; +} + +function wrapNativeClass(nativeClass: unknown): unknown { + if ( + !nativeClass || + (typeof nativeClass !== 'object' && typeof nativeClass !== 'function') + ) { + return nativeClass; + } + + const cached = nativeClassWrappers.get(nativeClass as object); + if (cached) { + return cached; + } + + const constructable = function NativeScriptNativeClass(...args: unknown[]) { + const cls = nativeClass as Record; + if (args.length > 0 && typeof cls.construct === 'function') { + return cls.construct(...args); + } + if (typeof cls.alloc !== 'function') { + throw new Error('Native class cannot be allocated'); + } + const instance = cls.alloc(); + if (instance && typeof instance.init === 'function') { + return instance.init(); + } + return instance; + }; + + Object.defineProperty(constructable, '__nativeApiClass', { + configurable: false, + enumerable: false, + writable: false, + value: nativeClass, + }); + + try { + const hasInstance = Symbol.hasInstance; + Object.defineProperty(constructable, hasInstance, { + configurable: true, + enumerable: false, + value(value: unknown) { + if (!value || typeof value !== 'object') { + return false; + } + + const cls = nativeClass as Record; + try { + if (typeof (value as Record).isKindOfClass === 'function') { + return Boolean((value as Record).isKindOfClass(constructable)); + } + } catch { + // Fall through to class-name equality for host objects that cannot + // dispatch isKindOfClass from this thread. + } + + const expectedName = cls.runtimeName ?? cls.name; + const actualName = (value as Record).className; + return typeof expectedName === 'string' && actualName === expectedName; + }, + }); + } catch { + // Older runtimes can expose Symbol.hasInstance as read-only. + } + + const wrapper = new Proxy(constructable, { + get(target, property, receiver) { + if (property in target) { + return Reflect.get(target, property, receiver); + } + return (nativeClass as Record)[property]; + }, + set(_target, property, value) { + (nativeClass as Record)[property] = value; + return true; + }, + has(target, property) { + return property in target || property in (nativeClass as object); + }, + }); + + nativeClassWrappers.set(nativeClass as object, wrapper); + return wrapper; +} + +function wrapInteropFactory( + nativeFactory: unknown, + properties: Record, +): unknown { + if (typeof nativeFactory !== 'function') { + return nativeFactory; + } + + if ((nativeFactory as Record).__nativeScriptConstructable) { + return nativeFactory; + } + + const constructable = function NativeScriptInteropValue(...args: unknown[]) { + return (nativeFactory as (...args: unknown[]) => unknown)(...args); + }; + + try { + const nativePrototype = (nativeFactory as {prototype?: unknown}).prototype; + if ( + nativePrototype && + (typeof nativePrototype === 'object' || typeof nativePrototype === 'function') + ) { + constructable.prototype = nativePrototype; + } + } catch { + // Keep construction working even if the host function exposes a fixed prototype. + } + + try { + const hasInstance = Symbol.hasInstance; + Object.defineProperty(constructable, hasInstance, { + configurable: true, + enumerable: false, + value(value: unknown) { + return ( + Boolean(value) && + typeof value === 'object' && + (value as Record).kind === properties.kind + ); + }, + }); + } catch { + // Older runtimes can expose Symbol.hasInstance as read-only. + } + + for (const [key, value] of Object.entries(properties)) { + try { + Object.defineProperty(constructable, key, { + configurable: true, + enumerable: false, + writable: false, + value, + }); + } catch { + // Best effort metadata copy for runtimes with stricter function objects. + } + } + + Object.defineProperty(constructable, '__nativeScriptConstructable', { + configurable: false, + enumerable: false, + writable: false, + value: true, + }); + + return constructable; +} + +function installInteropConstructors(): void { + const interop = (globalThis as Record).interop as + | Record + | undefined; + if (!interop || typeof interop !== 'object') { + return; + } + + const sizeof = interop.sizeof; + const pointerType = (interop.types as Record | undefined) + ?.pointer; + let pointerSize: unknown = undefined; + if (typeof sizeof === 'function' && pointerType !== undefined) { + try { + pointerSize = sizeof(pointerType); + } catch { + pointerSize = undefined; + } + } + + interop.Pointer = wrapInteropFactory(interop.Pointer, { + kind: 'pointer', + sizeof: pointerSize, + }); + interop.Reference = wrapInteropFactory(interop.Reference, { + kind: 'reference', + sizeof: pointerSize, + }); + interop.FunctionReference = wrapInteropFactory(interop.FunctionReference, { + kind: 'functionReference', + sizeof: pointerSize, + }); + + const types = interop.types as Record | undefined; + if (types && typeof types === 'object') { + for (const [name, value] of Object.entries(types)) { + if (typeof value !== 'number') { + continue; + } + const boxed = { + valueOf: () => value, + toString: () => String(value), + } as Record; + Object.defineProperty(boxed, nativeApiTypeCodeKey, { + configurable: false, + enumerable: false, + writable: false, + value, + }); + types[name] = boxed; + } + } +} + +function defineInlineFunction(name: string, value: Function): void { + if (Object.prototype.hasOwnProperty.call(globalThis, name)) { + return; + } + Object.defineProperty(globalThis, name, { + configurable: true, + enumerable: false, + writable: true, + value, + }); +} + +function installInlineFunctions(): void { + const makePoint = (x: number, y: number) => ({x, y}); + const makeSize = (width: number, height: number) => ({width, height}); + const makeRect = (x: number, y: number, width: number, height: number) => ({ + origin: {x, y}, + size: {width, height}, + }); + + defineInlineFunction('CGPointMake', makePoint); + defineInlineFunction('NSMakePoint', makePoint); + defineInlineFunction('CGSizeMake', makeSize); + defineInlineFunction('NSMakeSize', makeSize); + defineInlineFunction('CGRectMake', makeRect); + defineInlineFunction('NSMakeRect', makeRect); + defineInlineFunction('NSMakeRange', (location: number, length: number) => ({ + location, + length, + })); + defineInlineFunction( + 'UIEdgeInsetsMake', + (top: number, left: number, bottom: number, right: number) => ({ + top, + left, + bottom, + right, + }), + ); +} + +export function installGlobals(): boolean { + const api = nativeApiHost(); + if (!api) { + return false; + } + + const classNames = api.metadata?.classNames?.() ?? []; + for (const name of classNames) { + defineLazyNativeGlobal(name, (className) => wrapNativeClass(api[className])); + } + + const functionNames = api.metadata?.functionNames?.() ?? []; + for (const name of functionNames) { + defineLazyNativeGlobal(name, (functionName) => api[functionName]); + } + + const constantNames = api.metadata?.constantNames?.() ?? []; + for (const name of constantNames) { + defineLazyNativeGlobal(name, (constantName) => api[constantName]); + } + + const protocolNames = api.metadata?.protocolNames?.() ?? []; + for (const name of protocolNames) { + defineLazyNativeGlobal( + name, + (protocolName) => api.getProtocol?.(protocolName) ?? api[protocolName], + ); + } + + const enumNames = api.metadata?.enumNames?.() ?? []; + for (const name of enumNames) { + const resolveEnum = (enumName: string) => api.getEnum?.(enumName) ?? api[enumName]; + defineLazyNativeGlobal(name, resolveEnum); + + const enumValue = resolveEnum(name); + if (!enumValue || typeof enumValue !== 'object') { + continue; + } + for (const memberName of Object.keys(enumValue)) { + if (/^-?\d+$/.test(memberName)) { + continue; + } + defineLazyNativeGlobal( + memberName, + () => (enumValue as Record)[memberName], + ); + } + } + + const structNames = api.metadata?.structNames?.() ?? []; + for (const name of structNames) { + defineLazyNativeGlobal( + name, + (structName) => + wrapAggregateConstructor(api.getStruct?.(structName) ?? api[structName]), + true, + ); + } + + const unionNames = api.metadata?.unionNames?.() ?? []; + for (const name of unionNames) { + defineLazyNativeGlobal( + name, + (unionName) => + wrapAggregateConstructor(api.getUnion?.(unionName) ?? api[unionName]), + true, + ); + } + + return true; +} + +export function init( + metadataPath = '', + options: InstallOptions = {}, +): boolean { + const installed = NativeScriptNativeApi.isInstalled() + || NativeScriptNativeApi.install(metadataPath); + if (installed) { + installInteropConstructors(); + installInlineFunctions(); + } + if (installed && options.globals !== false) { + installGlobals(); + } + return installed; +} + +export const install = init; + +export function isInstalled(): boolean { + return NativeScriptNativeApi.isInstalled(); +} + +export function defaultMetadataPath(): string { + return NativeScriptNativeApi.defaultMetadataPath(); +} + +export function getRuntimeBackend(): string { + return NativeScriptNativeApi.getRuntimeBackend(); +} + +export function runOnUI(callback?: () => void): Promise { + const run = requireNativeApiHost().runOnUI; + if (typeof run !== 'function') { + throw new Error( + 'NativeScript Native API JSI host was installed without runOnUI', + ); + } + return run(callback); +} + +export function defineUIKitView( + definition: UIKitViewDefinition, +): UIKitViewComponent { + const Component = forwardRef, Props & ViewProps>( + function NativeScriptUIKitView(props, ref) { + const {nativeProps, pluginProps} = splitUIKitViewProps(props, definition); + const viewRef = useRef(null); + const propsRef = useRef(pluginProps); + const previousPropsRef = useRef | undefined>(); + const mountedRef = useRef(false); + const disposedRef = useRef(false); + const [nativeViewHandle, setNativeViewHandle] = useState(); + const [error, setError] = useState(null); + + propsRef.current = pluginProps; + + useImperativeHandle( + ref, + () => ({ + get nativeView() { + return viewRef.current; + }, + runOnUI(callback) { + return runOnUI(() => { + if (viewRef.current == null) { + throw new Error('UIKit view has not been created yet'); + } + callback(viewRef.current); + }); + }, + }), + [], + ); + + useEffect(() => { + disposedRef.current = false; + let cancelled = false; + + ensureNativeScriptInstalled(); + + runOnUI(() => { + const currentProps = propsRef.current; + const nativeView = definition.create(currentProps); + if (cancelled || disposedRef.current) { + definition.dispose?.(nativeView, currentProps); + const maybeView = nativeView as Record; + if (typeof maybeView.removeFromSuperview === 'function') { + maybeView.removeFromSuperview(); + } + return undefined; + } + viewRef.current = nativeView; + definition.update?.(nativeView, currentProps, undefined); + previousPropsRef.current = currentProps; + return undefined; + }) + .then(() => { + if (cancelled || viewRef.current == null) { + return; + } + setNativeViewHandle(nativeHandleForUIKitView(viewRef.current)); + }) + .catch((reason) => { + setError(reason instanceof Error ? reason : new Error(String(reason))); + }); + + return () => { + cancelled = true; + disposedRef.current = true; + const nativeView = viewRef.current; + viewRef.current = null; + mountedRef.current = false; + if (nativeView == null) { + return; + } + runOnUI(() => { + definition.dispose?.(nativeView, propsRef.current); + const maybeView = nativeView as Record; + if (typeof maybeView.removeFromSuperview === 'function') { + maybeView.removeFromSuperview(); + } + }).catch((reason) => { + setError(reason instanceof Error ? reason : new Error(String(reason))); + }); + }; + }, [definition]); + + useEffect(() => { + const nativeView = viewRef.current; + if (nativeView == null) { + return; + } + + const previousProps = previousPropsRef.current; + const currentProps = propsRef.current; + previousPropsRef.current = currentProps; + + runOnUI(() => { + definition.update?.(nativeView, currentProps, previousProps); + }).catch((reason) => { + setError(reason instanceof Error ? reason : new Error(String(reason))); + }); + }, [definition, pluginProps]); + + useEffect(() => { + const nativeView = viewRef.current; + if (nativeViewHandle == null || nativeView == null || mountedRef.current) { + return; + } + + mountedRef.current = true; + runOnUI(() => { + if (!disposedRef.current) { + definition.mounted?.(nativeView, propsRef.current); + } + }).catch((reason) => { + setError(reason instanceof Error ? reason : new Error(String(reason))); + }); + }, [definition, nativeViewHandle]); + + if (error) { + throw error; + } + + return React.createElement(NativeScriptUIViewNativeComponent, { + ...nativeProps, + collapsable: false, + nativeViewHandle, + }); + }, + ); + + Component.displayName = definition.displayName ?? 'NativeScriptUIKitView'; + return Component; +} + +const NativeScript = { + init, + install, + installGlobals, + isInstalled, + defaultMetadataPath, + defineUIKitView, + getRuntimeBackend, + runOnUI, +}; + +export default NativeScript; diff --git a/scripts/build_all_ios.sh b/scripts/build_all_ios.sh index 062bac94..8c4fa94f 100755 --- a/scripts/build_all_ios.sh +++ b/scripts/build_all_ios.sh @@ -24,7 +24,7 @@ for arg in $@; do --no-iphone|--no-device) BUILD_IPHONE=false ;; --macos) BUILD_MACOS=true ;; --no-macos) BUILD_MACOS=false ;; - --no-engine) TARGET_ENGINE=none ;; + --no-engine|--generic-napi) TARGET_ENGINE=none ;; --embed-metadata) EMBED_METADATA=true ;; *) ;; esac @@ -58,7 +58,7 @@ if $EMBED_METADATA; then checkpoint "... All metadata generated!" fi -"$SCRIPT_DIR/build_nativescript.sh" --no-vision $1 $2 $3 $4 $5 $6 $7 $8 $9 +"$SCRIPT_DIR/build_nativescript.sh" --no-vision "$@" if [[ "$TARGET_ENGINE" == "none" ]]; then # If you're building *with* --no-engine, you're trying to make an npm release diff --git a/scripts/build_all_macos.sh b/scripts/build_all_macos.sh index d333880d..9b09f9c9 100755 --- a/scripts/build_all_macos.sh +++ b/scripts/build_all_macos.sh @@ -10,6 +10,7 @@ if [ -z "$NO_UPDATE_VERSION" ]; then fi "$SCRIPT_DIR/build_metadata_generator.sh" +npm run metagen macos "$SCRIPT_DIR/build_nativescript.sh" --no-catalyst --no-iphone --no-sim --macos "$SCRIPT_DIR/build_tklivesync.sh" --no-catalyst --no-iphone --no-sim --no-vision --macos "$SCRIPT_DIR/prepare_dSYMs.sh" diff --git a/scripts/build_all_node_api.sh b/scripts/build_all_node_api.sh index 2a7c9422..18fa0b0b 100755 --- a/scripts/build_all_node_api.sh +++ b/scripts/build_all_node_api.sh @@ -54,7 +54,7 @@ fi checkpoint "... All metadata generated!" -"$SCRIPT_DIR/build_nativescript.sh" --no-vision --no-engine $1 $2 $3 $4 $5 $6 $7 $8 $9 +"$SCRIPT_DIR/build_nativescript.sh" --no-vision --no-engine "$@" "$SCRIPT_DIR/prepare_dSYMs.sh" "$SCRIPT_DIR/build_npm_node_api.sh" diff --git a/scripts/build_nativescript.sh b/scripts/build_nativescript.sh index 7de343ac..7f775ac8 100755 --- a/scripts/build_nativescript.sh +++ b/scripts/build_nativescript.sh @@ -14,7 +14,11 @@ EMBED_METADATA=$(to_bool ${EMBED_METADATA:=false}) CONFIG_BUILD=RelWithDebInfo TARGET_ENGINE=${TARGET_ENGINE:=v8} # default to v8 for compat +NS_FFI_BACKEND=${NS_FFI_BACKEND:=auto} +NS_GSD_BACKEND=${NS_GSD_BACKEND:=auto} METADATA_SIZE=${METADATA_SIZE:=0} +GENERATED_SIGNATURE_DISPATCH=${NS_SIGNATURE_BINDINGS_CPP_PATH:-${TNS_SIGNATURE_BINDINGS_CPP_PATH:-./NativeScript/ffi/napi/GeneratedSignatureDispatch.inc}} +GENERATED_SIGNATURE_DISPATCH_STAMP="${GENERATED_SIGNATURE_DISPATCH}.stamp" for arg in $@; do case $arg in @@ -39,6 +43,16 @@ for arg in $@; do --embed-metadata) EMBED_METADATA=true ;; --hermes) TARGET_ENGINE=hermes ;; --no-engine|--generic-napi) TARGET_ENGINE=none ;; + --ffi-direct) NS_FFI_BACKEND=direct ;; + --ffi-napi) NS_FFI_BACKEND=napi ;; + --ffi-backend=*) NS_FFI_BACKEND="${arg#--ffi-backend=}" ;; + --gsd-v8) NS_GSD_BACKEND=v8 ;; + --gsd-jsc) NS_GSD_BACKEND=jsc ;; + --gsd-quickjs) NS_GSD_BACKEND=quickjs ;; + --gsd-hermes) NS_GSD_BACKEND=hermes ;; + --gsd-napi) NS_GSD_BACKEND=napi ;; + --gsd-none) NS_GSD_BACKEND=none ;; + --gsd-backend=*) NS_GSD_BACKEND="${arg#--gsd-backend=}" ;; *) ;; esac done @@ -61,6 +75,111 @@ if ! $VERBOSE; then QUIET=-quiet fi +function assemble_node_api_xcframework () { + local output_dir="$1" + shift + + if command -v deno >/dev/null 2>&1; then + deno run -A ./scripts/build_xcframework.mts --output "$output_dir" "$@" + return + fi + + if [ ! -d "$SCRIPT_DIR/node_modules/react-native-node-api" ] || [ ! -d "$SCRIPT_DIR/node_modules/yargs-parser" ]; then + npm --prefix "$SCRIPT_DIR" install --no-audit --no-fund + fi + + node ./scripts/build_xcframework.mts --output "$output_dir" "$@" +} + +function effective_gsd_backend () { + local is_macos_napi="${1:-false}" + + if [ "$(effective_ffi_backend "$is_macos_napi")" == "direct" ]; then + echo none + return + fi + + case "$NS_GSD_BACKEND" in + auto) + if [ "$TARGET_ENGINE" == "none" ]; then + echo none + else + echo napi + fi + ;; + *) + echo "$NS_GSD_BACKEND" + ;; + esac +} + +function effective_ffi_backend () { + local is_macos_napi="${1:-false}" + + if $is_macos_napi || [ "$TARGET_ENGINE" == "none" ]; then + echo napi + return + fi + + case "$NS_FFI_BACKEND" in + auto) + if [[ "$TARGET_ENGINE" == "hermes" || "$TARGET_ENGINE" == "v8" || "$TARGET_ENGINE" == "jsc" || "$TARGET_ENGINE" == "quickjs" ]]; then + echo direct + else + echo napi + fi + ;; + *) + echo "$NS_FFI_BACKEND" + ;; + esac +} + +function signature_dispatch_stamp () { + local platform="$1" + local is_macos_napi="${2:-false}" + local backend + backend=$(effective_gsd_backend "$is_macos_napi") + local ffi_backend + ffi_backend=$(effective_ffi_backend "$is_macos_napi") + local generator_hash + generator_hash=$(find ./metadata-generator/src ./metadata-generator/include ./metadata-generator/CMakeLists.txt \ + -type f -print | LC_ALL=C sort | xargs shasum | shasum | awk '{print $1}') + printf "platform=%s\nbackend=%s\nffi_backend=%s\ntarget_engine=%s\nmetadata_size=%s\ngenerator_hash=%s\n" \ + "$platform" "$backend" "$ffi_backend" "$TARGET_ENGINE" "$METADATA_SIZE" "$generator_hash" +} + +function ensure_signature_dispatch_bindings () { + local platform="$1" + local is_macos_napi="${2:-false}" + local backend + backend=$(effective_gsd_backend "$is_macos_napi") + if [ "$TARGET_ENGINE" == "none" ] || [ "$backend" == "none" ]; then + return + fi + + if [ -z "$platform" ]; then + return + fi + + local expected_stamp + expected_stamp=$(signature_dispatch_stamp "$platform" "$is_macos_napi") + if [ -f "$GENERATED_SIGNATURE_DISPATCH" ] && \ + [ -f "$GENERATED_SIGNATURE_DISPATCH_STAMP" ] && \ + [ "$(cat "$GENERATED_SIGNATURE_DISPATCH_STAMP")" == "$expected_stamp" ]; then + return + fi + + if [ ! -x "./metadata-generator/dist/arm64/bin/objc-metadata-generator" ]; then + "$SCRIPT_DIR/build_metadata_generator.sh" + fi + + checkpoint "Generating signature dispatch bindings for $platform ($backend)..." + NS_SIGNATURE_BINDINGS_CPP_PATH="$GENERATED_SIGNATURE_DISPATCH" npm run metagen "$platform" + mkdir -p "$(dirname "$GENERATED_SIGNATURE_DISPATCH_STAMP")" + printf "%s" "$expected_stamp" > "$GENERATED_SIGNATURE_DISPATCH_STAMP" +} + DEV_TEAM=${DEVELOPMENT_TEAM:-} DIST=$(PWD)/dist mkdir -p $DIST @@ -84,6 +203,8 @@ function cmake_build () { is_macos_napi=true fi + ensure_signature_dispatch_bindings "$platform" "$is_macos_napi" + local libffi_build_dir= case "$platform" in ios) libffi_build_dir="iphoneos-arm64" ;; @@ -102,10 +223,26 @@ function cmake_build () { local cache_file="$build_dir/CMakeCache.txt" if [ -f "$cache_file" ]; then + local needs_reconfigure=false local cached_engine - cached_engine=$(grep '^TARGET_ENGINE:STRING=' "$cache_file" | sed 's/^TARGET_ENGINE:STRING=//') + cached_engine=$(grep '^TARGET_ENGINE:STRING=' "$cache_file" | sed 's/^TARGET_ENGINE:STRING=//' || true) if [ -n "$cached_engine" ] && [ "$cached_engine" != "$TARGET_ENGINE" ]; then echo "Reconfiguring $platform build directory for engine '$TARGET_ENGINE' (was '$cached_engine')." + needs_reconfigure=true + fi + local cached_gsd_backend + cached_gsd_backend=$(grep '^NS_GSD_BACKEND:STRING=' "$cache_file" | sed 's/^NS_GSD_BACKEND:STRING=//' || true) + if [ -n "$cached_gsd_backend" ] && [ "$cached_gsd_backend" != "$NS_GSD_BACKEND" ]; then + echo "Reconfiguring $platform build directory for GSD backend '$NS_GSD_BACKEND' (was '$cached_gsd_backend')." + needs_reconfigure=true + fi + local cached_ffi_backend + cached_ffi_backend=$(grep '^NS_FFI_BACKEND:STRING=' "$cache_file" | sed 's/^NS_FFI_BACKEND:STRING=//' || true) + if [ -n "$cached_ffi_backend" ] && [ "$cached_ffi_backend" != "$NS_FFI_BACKEND" ]; then + echo "Reconfiguring $platform build directory for FFI backend '$NS_FFI_BACKEND' (was '$cached_ffi_backend')." + needs_reconfigure=true + fi + if $needs_reconfigure; then rm -rf "$build_dir" fi fi @@ -122,7 +259,7 @@ function cmake_build () { fi - cmake -S=./NativeScript -B="$build_dir" -GXcode -DTARGET_PLATFORM=$platform -DTARGET_ENGINE=$TARGET_ENGINE -DMETADATA_SIZE=$METADATA_SIZE -DBUILD_CLI_BINARY=$is_macos_cli -DBUILD_MACOS_NODE_API=$is_macos_napi + cmake -S=./NativeScript -B="$build_dir" -GXcode -DTARGET_PLATFORM=$platform -DTARGET_ENGINE=$TARGET_ENGINE -DNS_FFI_BACKEND=$NS_FFI_BACKEND -DNS_GSD_BACKEND=$NS_GSD_BACKEND -DMETADATA_SIZE=$METADATA_SIZE -DBUILD_CLI_BINARY=$is_macos_cli -DBUILD_MACOS_NODE_API=$is_macos_napi cmake --build "$build_dir" --config $CONFIG_BUILD -- \ CODE_SIGN_STYLE=Manual \ @@ -223,7 +360,7 @@ if [[ -n "${XCFRAMEWORKS[@]}" ]]; then # https://github.com/callstackincubator/react-native-node-api/blob/9b231c14459b62d7df33360f930a00343d8c46e6/docs/PREBUILDS.md OUTPUT_DIR="packages/ios-node-api/build/$CONFIG_BUILD/NativeScript.apple.node" rm -rf $OUTPUT_DIR - deno run -A ./scripts/build_xcframework.mts --output "$OUTPUT_DIR" ${XCFRAMEWORKS[@]} + assemble_node_api_xcframework "$OUTPUT_DIR" "${XCFRAMEWORKS[@]}" else checkpoint "Creating NativeScript.xcframework" @@ -246,7 +383,7 @@ if $BUILD_MACOS; then # https://github.com/callstackincubator/react-native-node-api/blob/9b231c14459b62d7df33360f930a00343d8c46e6/docs/PREBUILDS.md OUTPUT_DIR="packages/macos-node-api/build/$CONFIG_BUILD/NativeScript.apple.node" rm -rf $OUTPUT_DIR - deno run -A ./scripts/build_xcframework.mts --output "$OUTPUT_DIR" ${XCFRAMEWORKS[@]} + assemble_node_api_xcframework "$OUTPUT_DIR" "${XCFRAMEWORKS[@]}" fi fi diff --git a/scripts/build_npm_ios.sh b/scripts/build_npm_ios.sh index afe6a3a9..41587347 100755 --- a/scripts/build_npm_ios.sh +++ b/scripts/build_npm_ios.sh @@ -12,13 +12,27 @@ if [ ! -f "$PACKAGE_DIR/package.json" ]; then fi OUTPUT_DIR="$PACKAGE_DIR/dist" STAGING_DIR="$OUTPUT_DIR/package" +PACKAGE_NAME_OVERRIDE=${NPM_PACKAGE_NAME:-} +PACKAGE_VERSION_OVERRIDE=${NPM_PACKAGE_VERSION:-} +PACK_DESTINATION=${NPM_PACK_DESTINATION:-..} rm -rf "$OUTPUT_DIR" mkdir -p "$STAGING_DIR/framework/internal" +mkdir -p "$PACK_DESTINATION" cp "$PACKAGE_DIR/package.json" "$STAGING_DIR" cp "$PACKAGE_DIR/README.md" "$STAGING_DIR" cp "$PACKAGE_DIR/LICENSE" "$STAGING_DIR" +if [ -n "$PACKAGE_NAME_OVERRIDE" ] || [ -n "$PACKAGE_VERSION_OVERRIDE" ]; then + TMP_FILE=$(mktemp) + jq \ + --arg name "$PACKAGE_NAME_OVERRIDE" \ + --arg version "$PACKAGE_VERSION_OVERRIDE" \ + 'if $name != "" then .name = $name else . end | if $version != "" then .version = $version else . end' \ + "$STAGING_DIR/package.json" > "$TMP_FILE" + mv "$TMP_FILE" "$STAGING_DIR/package.json" +fi + cp -R "./templates/ios/." "$STAGING_DIR/framework" cp -R "dist/NativeScript.xcframework" "$STAGING_DIR/framework/internal" @@ -39,7 +53,7 @@ cp -R "metadata-generator/dist/arm64/." "$STAGING_DIR/framework/internal/metadat ) pushd "$STAGING_DIR" -npm pack --pack-destination .. +npm pack --pack-destination "$PACK_DESTINATION" popd checkpoint "npm package created." diff --git a/scripts/build_react_native_turbomodule.sh b/scripts/build_react_native_turbomodule.sh new file mode 100755 index 00000000..f64f23bf --- /dev/null +++ b/scripts/build_react_native_turbomodule.sh @@ -0,0 +1,186 @@ +#!/bin/bash +set -euo pipefail +source "$(dirname "$0")/build_utils.sh" + +PACKAGE_DIR="packages/react-native" +OUTPUT_DIR="$PACKAGE_DIR/dist" +PACK_DESTINATION=${NPM_PACK_DESTINATION:-"$REPO_ROOT/build/npm-tarballs"} +VERSION_OVERRIDE=${NPM_PACKAGE_VERSION:-} +SKIP_PACK=false + +while [[ $# -gt 0 ]]; do + case "$1" in + --no-pack) + SKIP_PACK=true + shift + ;; + *) + echo "Unknown argument: $1" >&2 + exit 1 + ;; + esac +done + +checkpoint "Preparing @nativescript/react-native TurboModule package..." + +rm -rf \ + "$PACKAGE_DIR/native-api-jsi" \ + "$PACKAGE_DIR/metadata" \ + "$PACKAGE_DIR/ios/vendor" \ + "$PACKAGE_DIR/types" +mkdir -p \ + "$PACKAGE_DIR/native-api-jsi/jsi" \ + "$PACKAGE_DIR/native-api-jsi/metadata/include" \ + "$PACKAGE_DIR/metadata" \ + "$PACKAGE_DIR/ios/vendor/libffi/include" \ + "$PACKAGE_DIR/types/ios" \ + "$PACKAGE_DIR/types/objc-node-api" \ + "$PACK_DESTINATION" + +cp NativeScript/ffi/hermes/jsi/NativeApiJsi.h "$PACKAGE_DIR/native-api-jsi/" +cp NativeScript/ffi/hermes/jsi/NativeApiJsi.mm "$PACKAGE_DIR/native-api-jsi/" +cp NativeScript/ffi/shared/jsi/NativeApiJsi*.h "$PACKAGE_DIR/native-api-jsi/jsi/" +cp NativeScript/ffi/hermes/jsi/NativeApiJsiReactNative.h "$PACKAGE_DIR/native-api-jsi/" +cp metadata-generator/include/Metadata.h "$PACKAGE_DIR/native-api-jsi/metadata/include/" +cp metadata-generator/include/MetadataReader.h "$PACKAGE_DIR/native-api-jsi/metadata/include/" +cp NativeScript/libffi/iphonesimulator-universal/include/ffi.h "$PACKAGE_DIR/ios/vendor/libffi/include/" +cp NativeScript/libffi/iphonesimulator-universal/include/ffitarget.h "$PACKAGE_DIR/ios/vendor/libffi/include/" + +cp metadata-generator/metadata/metadata.ios-sim.arm64.nsmd "$PACKAGE_DIR/metadata/" +cp metadata-generator/metadata/metadata.ios-sim.x86_64.nsmd "$PACKAGE_DIR/metadata/" +cp metadata-generator/metadata/metadata.ios.arm64.nsmd "$PACKAGE_DIR/metadata/" + +checkpoint "Staging iOS SDK TypeScript declarations..." +cp packages/objc-node-api/index.d.ts "$PACKAGE_DIR/types/objc-node-api/" +cp packages/objc-node-api/inline_functions.d.ts "$PACKAGE_DIR/types/objc-node-api/" +cp packages/ios/types/*.d.ts "$PACKAGE_DIR/types/ios/" +perl -0pi -e 's#/// #/// #g' \ + "$PACKAGE_DIR"/types/ios/*.d.ts +node - "$PACKAGE_DIR/types/ios" <<'NODE' +const fs = require('fs'); +const path = require('path'); + +const typesDir = process.argv[2]; +const reservedParameterNames = [ + 'abstract', + 'as', + 'asserts', + 'async', + 'await', + 'break', + 'case', + 'catch', + 'class', + 'const', + 'continue', + 'debugger', + 'declare', + 'default', + 'delete', + 'do', + 'else', + 'enum', + 'export', + 'extends', + 'false', + 'finally', + 'for', + 'from', + 'function', + 'if', + 'implements', + 'import', + 'in', + 'infer', + 'instanceof', + 'interface', + 'is', + 'keyof', + 'let', + 'module', + 'namespace', + 'never', + 'new', + 'null', + 'of', + 'package', + 'private', + 'protected', + 'public', + 'readonly', + 'require', + 'return', + 'satisfies', + 'static', + 'super', + 'switch', + 'throw', + 'true', + 'try', + 'type', + 'typeof', + 'undefined', + 'unique', + 'unknown', + 'var', + 'void', + 'while', + 'with', + 'yield', +]; +const reservedPattern = new RegExp( + `([,(]\\s*)(${reservedParameterNames.join('|')})(\\??\\s*:)`, + 'g', +); + +for (const entry of fs.readdirSync(typesDir)) { + if (!entry.endsWith('.d.ts')) { + continue; + } + + const file = path.join(typesDir, entry); + let source = fs.readFileSync(file, 'utf8'); + source = source.replace(reservedPattern, '$1_$2$3'); + source = source.replace(/^[ \t]*:[^;\n]+;[ \t]*\n/gm, ''); + fs.writeFileSync(file, source); +} +NODE +{ + echo '/// ' + while IFS= read -r declaration; do + echo "/// " + done < <(find "$PACKAGE_DIR/types/ios" -maxdepth 1 -name '*.d.ts' ! -name 'index.d.ts' -exec basename {} \; | sort) +} > "$PACKAGE_DIR/types/ios/index.d.ts" + +checkpoint "Creating Libffi.xcframework for the TurboModule pod..." +xcodebuild -create-xcframework \ + -library NativeScript/libffi/iphoneos-arm64/libffi.a \ + -headers NativeScript/libffi/iphoneos-arm64/include \ + -library NativeScript/libffi/iphonesimulator-universal/libffi.a \ + -headers NativeScript/libffi/iphonesimulator-universal/include \ + -output "$PACKAGE_DIR/ios/vendor/Libffi.xcframework" + +if [[ -n "$VERSION_OVERRIDE" ]]; then + TMP_FILE=$(mktemp) + jq --arg version "$VERSION_OVERRIDE" '.version = $version' \ + "$PACKAGE_DIR/package.json" > "$TMP_FILE" + mv "$TMP_FILE" "$PACKAGE_DIR/package.json" +fi + +if [[ "$SKIP_PACK" == "true" ]]; then + checkpoint "@nativescript/react-native package staged." + exit 0 +fi + +rm -rf "$OUTPUT_DIR" +mkdir -p "$OUTPUT_DIR" + +checkpoint "Packing @nativescript/react-native..." +( + cd "$PACKAGE_DIR" + npm pack --pack-destination "$REPO_ROOT/$OUTPUT_DIR" +) + +cp "$OUTPUT_DIR"/*.tgz "$PACK_DESTINATION/" + +checkpoint "@nativescript/react-native npm package created." diff --git a/scripts/check_ffi_boundaries.sh b/scripts/check_ffi_boundaries.sh new file mode 100755 index 00000000..62a06ca4 --- /dev/null +++ b/scripts/check_ffi_boundaries.sh @@ -0,0 +1,44 @@ +#!/bin/bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +NAPI_ENGINE_DIR="$ROOT_DIR/NativeScript/ffi/napi/engine" +DIRECT_DIRS=( + "$ROOT_DIR/NativeScript/ffi/hermes" + "$ROOT_DIR/NativeScript/ffi/v8" + "$ROOT_DIR/NativeScript/ffi/jsc" + "$ROOT_DIR/NativeScript/ffi/quickjs" + "$ROOT_DIR/NativeScript/ffi/shared" + "$ROOT_DIR/packages/react-native/native-api-jsi" +) + +if [ -d "$NAPI_ENGINE_DIR" ] && find "$NAPI_ENGINE_DIR" -type f | grep -q .; then + echo "ffi/napi must remain a pure Node-API backend; do not add ffi/napi/engine." >&2 + exit 1 +fi + +EXISTING_DIRECT_DIRS=() +for dir in "${DIRECT_DIRS[@]}"; do + if [ -d "$dir" ]; then + EXISTING_DIRECT_DIRS+=("$dir") + fi +done + +if [ "${#EXISTING_DIRECT_DIRS[@]}" -eq 0 ]; then + exit 0 +fi + +if rg -n '\b(napi_|napi_env|napi_value|js_native_api|node_api)\b' \ + "${EXISTING_DIRECT_DIRS[@]}" \ + -g '*.{h,hh,hpp,c,cc,cpp,m,mm,inc}'; then + echo "Node-API symbols are not allowed in shared or direct engine FFI folders." >&2 + exit 1 +fi + +if rg -n '\b(EngineDirect|FastNative|HermesFast|V8Fast|JSCFast|QuickJSFast)\b' \ + "$ROOT_DIR/NativeScript/ffi/napi" \ + -g '*.{h,hh,hpp,c,cc,cpp,m,mm,inc}' \ + -g '!GeneratedSignatureDispatch.inc'; then + echo "Direct-engine FFI code is not allowed in ffi/napi." >&2 + exit 1 +fi diff --git a/scripts/create_react_native_demo.sh b/scripts/create_react_native_demo.sh new file mode 100755 index 00000000..dc89a20b --- /dev/null +++ b/scripts/create_react_native_demo.sh @@ -0,0 +1,52 @@ +#!/bin/bash +set -euo pipefail +source "$(dirname "$0")/build_utils.sh" +source "$SCRIPT_DIR/react_native_app_utils.sh" + +RN_VERSION=${RN_DEMO_VERSION:-0.85.3} +RN_CLI_VERSION=${RN_DEMO_CLI_VERSION:-20.1.3} +APP_NAME=${RN_DEMO_APP_NAME:-NativeScriptNativeApiDemo} +APP_ROOT=${RN_DEMO_APP_ROOT:-"$REPO_ROOT/build/react-native-demo"} +APP_DIR="$APP_ROOT/$APP_NAME" +CONFIGURATION=${IOS_CONFIGURATION:-Release} +FORCE_RECREATE=${RN_DEMO_FORCE_RECREATE:-0} +RUN_BUILD=${RN_DEMO_BUILD:-1} +RUN_LAUNCH=${RN_DEMO_LAUNCH:-1} +BUILD_TIMEOUT_SECONDS=${RN_DEMO_BUILD_TIMEOUT_SECONDS:-1800} +LAUNCH_TIMEOUT_SECONDS=${RN_DEMO_LAUNCH_TIMEOUT_SECONDS:-90} +BUNDLE_ID="org.reactjs.native.example.$APP_NAME" +MARKER="NATIVESCRIPT_RN_TURBO_DEMO_PASS" +MARKER_FILE_NAME="NativeScriptNativeApiSmoke.marker" +DEMO_APP_TSX="$REPO_ROOT/examples/react-native-demo/App.tsx" + +rn_build_turbo_tarball +TARBALL=$(rn_latest_turbo_tarball) + +if [[ "$FORCE_RECREATE" == "1" ]]; then + rm -rf "$APP_DIR" +fi + +rn_create_app_if_missing "$APP_DIR" "$APP_ROOT" "$APP_NAME" "$RN_VERSION" "$RN_CLI_VERSION" "React Native demo app" +rn_install_turbo_tarball "$APP_DIR" "$TARBALL" "demo app" + +checkpoint "Installing demo entrypoint..." +cp "$DEMO_APP_TSX" "$APP_DIR/App.tsx" + +rn_install_pods "$APP_DIR" "demo app" + +if [[ "$RUN_BUILD" != "1" ]]; then + checkpoint "React Native demo app is ready at $APP_DIR" + exit 0 +fi + +UDID=$(rn_require_ios_simulator) +rn_build_ios_app "$APP_DIR" "$APP_ROOT" "$APP_NAME" "$CONFIGURATION" "$UDID" "$BUILD_TIMEOUT_SECONDS" "demo app" +APP_BUNDLE="$RN_APP_BUNDLE" + +if [[ "$RUN_LAUNCH" == "1" ]]; then + checkpoint "Launching demo app..." + MARKER_FILE=$(rn_launch_app_with_marker "$UDID" "$APP_BUNDLE" "$BUNDLE_ID" "$MARKER_FILE_NAME") + rn_wait_for_marker_file "$MARKER_FILE" "$MARKER" "$LAUNCH_TIMEOUT_SECONDS" +fi + +checkpoint "React Native NativeScript demo app is ready at $APP_DIR" diff --git a/scripts/get-npm-tag.js b/scripts/get-npm-tag.js index 7555e2a7..046c37ee 100644 --- a/scripts/get-npm-tag.js +++ b/scripts/get-npm-tag.js @@ -16,6 +16,7 @@ if (!currentVersion) { "ios-node-api": "../packages/ios-node-api/package.json", "macos-node-api": "../packages/macos-node-api/package.json", "objc-node-api": "../packages/objc-node-api/package.json", + "react-native": "../packages/react-native/package.json", }; if (!packageJsonByTarget[target]) { diff --git a/scripts/metagen.js b/scripts/metagen.js index bf3bc286..be4b5014 100755 --- a/scripts/metagen.js +++ b/scripts/metagen.js @@ -313,9 +313,14 @@ async function main() { const typesDir = path.resolve(__dirname, "..", "packages", sdkName, "types"); const metadataDir = path.resolve(__dirname, "..", "metadata-generator", "metadata"); + const signatureBindingsPath = + process.env.NS_SIGNATURE_BINDINGS_CPP_PATH || + process.env.TNS_SIGNATURE_BINDINGS_CPP_PATH || + path.resolve(__dirname, "..", "NativeScript", "ffi", "napi", "GeneratedSignatureDispatch.inc"); await fsp.rm(typesDir, { recursive: true, force: true }); await fsp.mkdir(typesDir, { recursive: true }); await fsp.mkdir(metadataDir, { recursive: true }); + await fsp.mkdir(path.dirname(signatureBindingsPath), { recursive: true }); for (const arch of Object.keys(sdk.targets)) { // Use the matching arch binary when available, falling back to arm64. @@ -367,6 +372,8 @@ async function main() { metadataDir, `metadata.${sdkName}.${arch}.h`, ), + "-output-signature-bindings-cpp", + signatureBindingsPath, "Xclang", "-isysroot", sdk.path, diff --git a/scripts/react_native_app_utils.sh b/scripts/react_native_app_utils.sh new file mode 100644 index 00000000..a2d0553e --- /dev/null +++ b/scripts/react_native_app_utils.sh @@ -0,0 +1,188 @@ +#!/bin/bash +set -euo pipefail + +# Shared helpers for generated React Native apps used by smoke tests, FFI +# compatibility tests, and local demos. Source build_utils.sh before this file. + +function rn_build_turbo_tarball() { + checkpoint "Building @nativescript/react-native TurboModule tarball..." + "$SCRIPT_DIR/build_react_native_turbomodule.sh" +} + +function rn_latest_turbo_tarball() { + ls -t "$REPO_ROOT/packages/react-native/dist"/*.tgz | head -n 1 +} + +function rn_create_app_if_missing() { + local app_dir="$1" + local app_root="$2" + local app_name="$3" + local rn_version="$4" + local rn_cli_version="$5" + local label="$6" + + if [[ ! -d "$app_dir" ]]; then + checkpoint "Creating $label ($rn_version)..." + mkdir -p "$app_root" + npx --yes "@react-native-community/cli@$rn_cli_version" init "$app_name" \ + --version "$rn_version" \ + --directory "$app_dir" \ + --skip-git-init \ + --install-pods false \ + --pm npm + fi +} + +function rn_install_turbo_tarball() { + local app_dir="$1" + local tarball="$2" + local label="$3" + + checkpoint "Installing local TurboModule tarball into $label..." + ( + cd "$app_dir" + npm install "$tarball" + ) +} + +function rn_install_pods() { + local app_dir="$1" + local label="$2" + + checkpoint "Installing CocoaPods for $label..." + ( + cd "$app_dir/ios" + if [[ -f Gemfile ]]; then + bundle install + RCT_NEW_ARCH_ENABLED=1 USE_HERMES=1 bundle exec pod install + else + RCT_NEW_ARCH_ENABLED=1 USE_HERMES=1 pod install + fi + ) +} + +function rn_select_ios_simulator() { + node <<'NODE' +const cp = require('child_process'); +const devices = JSON.parse(cp.execFileSync('xcrun', ['simctl', 'list', 'devices', 'available', '--json'], {encoding: 'utf8'})); +const runtimes = Object.keys(devices.devices).filter((runtime) => runtime.includes('iOS')).sort().reverse(); +for (const runtime of runtimes) { + const booted = devices.devices[runtime].find((device) => device.state === 'Booted' && device.name.includes('iPhone')); + if (booted) { + console.log(booted.udid); + process.exit(0); + } +} +for (const runtime of runtimes) { + const candidate = devices.devices[runtime].find((device) => device.name.includes('iPhone')); + if (candidate) { + console.log(candidate.udid); + process.exit(0); + } +} +process.exit(1); +NODE +} + +function rn_require_ios_simulator() { + local udid + if ! udid=$(rn_select_ios_simulator); then + udid="" + fi + if [[ -z "$udid" ]]; then + echo "No available iOS simulator found." >&2 + exit 1 + fi + echo "$udid" +} + +function rn_build_ios_app() { + local app_dir="$1" + local app_root="$2" + local app_name="$3" + local configuration="$4" + local udid="$5" + local timeout_seconds="$6" + local label="$7" + + checkpoint "Building $label for simulator..." + xcrun simctl boot "$udid" >/dev/null 2>&1 || true + xcrun simctl bootstatus "$udid" -b + + xcodebuild \ + -workspace "$app_dir/ios/$app_name.xcworkspace" \ + -scheme "$app_name" \ + -configuration "$configuration" \ + -sdk iphonesimulator \ + -destination "platform=iOS Simulator,id=$udid" \ + -derivedDataPath "$app_dir/ios/build/DerivedData" \ + ONLY_ACTIVE_ARCH=YES \ + build | tee "$app_root/xcodebuild.log" & + local build_pid=$! + + local seconds_waited=0 + while kill -0 "$build_pid" >/dev/null 2>&1; do + if [[ "$seconds_waited" -ge "$timeout_seconds" ]]; then + kill "$build_pid" >/dev/null 2>&1 || true + echo "$label build timed out after ${timeout_seconds}s." >&2 + exit 1 + fi + sleep 5 + seconds_waited=$((seconds_waited + 5)) + done + wait "$build_pid" + + RN_APP_BUNDLE=$(find "$app_dir/ios/build/DerivedData/Build/Products/$configuration-iphonesimulator" -maxdepth 1 -name "$app_name.app" -print -quit) + if [[ -z "$RN_APP_BUNDLE" ]]; then + echo "Built app bundle not found." >&2 + exit 1 + fi +} + +function rn_launch_app_with_marker() { + local udid="$1" + local app_bundle="$2" + local bundle_id="$3" + local marker_file_name="$4" + + xcrun simctl install "$udid" "$app_bundle" + local data_container + data_container=$(xcrun simctl get_app_container "$udid" "$bundle_id" data) + local marker_file="$data_container/tmp/$marker_file_name" + rm -f "$marker_file" + + SIMCTL_CHILD_NATIVESCRIPT_RN_TURBO_SMOKE_MARKER=1 \ + xcrun simctl launch --terminate-running-process "$udid" "$bundle_id" >/dev/null + + echo "$marker_file" +} + +function rn_wait_for_marker_file() { + local marker_file="$1" + local marker="$2" + local timeout_seconds="$3" + + node - "$marker_file" "$marker" "$timeout_seconds" <<'NODE' +const fs = require('fs'); +const [markerFile, marker, timeoutSecondsText] = process.argv.slice(2); +const timeoutMs = Number(timeoutSecondsText) * 1000; +const startedAt = Date.now(); + +function poll() { + if (fs.existsSync(markerFile)) { + const content = fs.readFileSync(markerFile, 'utf8'); + console.log(`${marker} ${JSON.stringify({markerFile, content: content.trim()})}`); + process.exit(0); + } + + if (Date.now() - startedAt > timeoutMs) { + console.error(`Timed out waiting for ${marker} file at ${markerFile}.`); + process.exit(1); + } + + setTimeout(poll, 2000); +} + +poll(); +NODE +} diff --git a/scripts/run-tests-ios.js b/scripts/run-tests-ios.js index debfa5de..6aaca1ea 100644 --- a/scripts/run-tests-ios.js +++ b/scripts/run-tests-ios.js @@ -13,6 +13,7 @@ // artifacts need rebuilding. Supported: v8, hermes, quickjs, jsc. Defaults to v8. // - IOS_SWIFT_VERSION overrides default Swift version (default: 5.0). // - IOS_COMMAND_TIMEOUT_MS overrides timeout for build/install/simctl commands (default: 3 minutes). +// - IOS_SIMCTL_QUERY_TIMEOUT_MS overrides timeout for polling simctl queries (default: 10 seconds). // - IOS_BUILD_TIMEOUT_MS overrides timeout for xcodebuild app build (default: IOS_COMMAND_TIMEOUT_MS). // - IOS_COMMAND_MAX_BUFFER_BYTES overrides spawnSync maxBuffer for captured command output (default: 64 MiB). // - IOS_TEST_TIMEOUT_MS overrides max test runtime (default: 10 minutes). @@ -95,6 +96,10 @@ const commandTimeoutMs = parseTimeoutMs("IOS_COMMAND_TIMEOUT_MS", 3 * 60 * 1000) // Clean CI runners often need substantially longer for the first iOS build than // for simulator control commands like boot/install/log collection. const buildTimeoutMs = parseTimeoutMs("IOS_BUILD_TIMEOUT_MS", 10 * 60 * 1000); +const simctlQueryTimeoutMs = parseTimeoutMs( + "IOS_SIMCTL_QUERY_TIMEOUT_MS", + Math.min(commandTimeoutMs, 10 * 1000) +); const commandMaxBufferBytes = parsePositiveInt("IOS_COMMAND_MAX_BUFFER_BYTES", 64 * 1024 * 1024); const testTimeoutMs = Number(process.env.IOS_TEST_TIMEOUT_MS || 10 * 60 * 1000); const inactivityTimeoutMs = Number(process.env.IOS_TEST_INACTIVITY_TIMEOUT_MS || 2 * 60 * 1000); @@ -634,13 +639,25 @@ function buildTestRunnerApp(destination, swiftVersion) { return { appPath, reusedBuild: canReuseBuild }; } +const appContainerPathCache = new Map(); + function getAppContainerPath(udid, containerType) { - const result = run("xcrun", ["simctl", "get_app_container", udid, bundleId, containerType]); + const cacheKey = `${udid}:${containerType}`; + if (appContainerPathCache.has(cacheKey)) { + return appContainerPathCache.get(cacheKey); + } + + const result = run("xcrun", ["simctl", "get_app_container", udid, bundleId, containerType], { + timeout: simctlQueryTimeoutMs + }); if (result.status !== 0) { return null; } const out = (result.stdout || "").trim(); + if (out) { + appContainerPathCache.set(cacheKey, out); + } return out || null; } @@ -882,7 +899,8 @@ async function waitForCompletedJunitOrLaunchExit(udid, launchProcess, timeoutMs, if (Date.now() - state.lastActivityAt >= inactivityTimeoutMs) { const launchPid = extractLaunchPid(state.logs); - if (!isAppProcessRunning(udid, launchPid)) { + const appRunning = isAppProcessRunning(udid, launchPid); + if (appRunning === false) { return { junitResult: null, launchResult, timedOut: true, inactive: true }; } } @@ -1002,9 +1020,11 @@ function readJunitFileState(udid) { } function collectSimulatorProcessSnapshot(udid) { - const result = run("xcrun", ["simctl", "spawn", udid, "ps", "-axo", "pid,ppid,stat,etime,command"]); + const result = run("xcrun", ["simctl", "spawn", udid, "ps", "-axo", "pid,ppid,stat,etime,command"], { + timeout: simctlQueryTimeoutMs + }); if (result.status !== 0) { - return ""; + return null; } const lines = (result.stdout || "") @@ -1016,6 +1036,9 @@ function collectSimulatorProcessSnapshot(udid) { function isAppProcessRunning(udid, pid) { const snapshot = collectSimulatorProcessSnapshot(udid); + if (snapshot == null) { + return null; + } if (!snapshot) { return false; } diff --git a/scripts/run-tests-macos.js b/scripts/run-tests-macos.js index 30e4c11b..09435c34 100644 --- a/scripts/run-tests-macos.js +++ b/scripts/run-tests-macos.js @@ -7,12 +7,16 @@ // - MACOS_TEST_CLEAN_BUILD=1 deletes derived data before build. // - MACOS_TEST_ENGINE selects the runtime engine build to use when runtime // artifacts need rebuilding. Supported: v8, hermes, quickjs, jsc. Defaults to v8. +// - MACOS_TEST_FFI_BACKEND selects the FFI backend build to use when runtime +// artifacts need rebuilding. Supported: auto, napi, direct. Defaults to auto. // - MACOS_COMMAND_TIMEOUT_MS overrides timeout for build commands (default: 10 minutes). // - MACOS_COMMAND_MAX_BUFFER_BYTES overrides spawnSync maxBuffer for captured command output (default: 64 MiB). // - MACOS_TEST_TIMEOUT_MS overrides max test runtime after launch (default: 2 minutes). // - MACOS_TEST_INACTIVITY_TIMEOUT_MS overrides max no-log interval after launch (default: 45 seconds). // - MACOS_LOG_JUNIT=0 disables streaming TKUnit/JUnit lines to console. // - MACOS_TESTS filters test modules (comma-separated substrings passed as -tests). +// - MACOS_TEST_SPECS filters spec names (comma-separated substrings passed as -specs). +// - MACOS_TEST_VERBOSE_SPECS=1 prints Jasmine spec start/done markers. const fs = require("fs"); const path = require("path"); @@ -87,13 +91,20 @@ const testTimeoutMs = parseTimeoutMs("MACOS_TEST_TIMEOUT_MS", 2 * 60 * 1000); const inactivityTimeoutMs = parseTimeoutMs("MACOS_TEST_INACTIVITY_TIMEOUT_MS", 45 * 1000); const emitJunitLogs = process.env.MACOS_LOG_JUNIT !== "0"; const requestedTests = (process.env.MACOS_TESTS || "").trim(); +const requestedSpecs = (process.env.MACOS_TEST_SPECS || "").trim(); +const verboseSpecs = process.env.MACOS_TEST_VERBOSE_SPECS === "1"; const requestedEngine = (process.env.MACOS_TEST_ENGINE || "v8").trim().toLowerCase(); +const requestedFfiBackend = (process.env.MACOS_TEST_FFI_BACKEND || "auto").trim().toLowerCase(); const launchedMarker = "Application Start!"; const junitPrefix = "TKUnit: "; const junitEndTag = ""; const consoleLogMarker = "CONSOLE LOG:"; const crashReportsDir = path.join(os.homedir(), "Library", "Logs", "DiagnosticReports"); +const generatedRuntimeBuildOutputs = new Set([ + path.join(nativeScriptSourceRoot, "ffi", "napi", "GeneratedSignatureDispatch.inc"), + path.join(nativeScriptSourceRoot, "ffi", "napi", "GeneratedSignatureDispatch.inc.stamp") +]); function parseArgs() { const args = process.argv.slice(2).filter(Boolean); @@ -136,6 +147,9 @@ function getPathStats(targetPath) { while (queue.length > 0) { const currentPath = queue.pop(); + if (generatedRuntimeBuildOutputs.has(currentPath)) { + continue; + } let stats; try { stats = fs.lstatSync(currentPath); @@ -469,6 +483,12 @@ function ensureMacOSRuntimeArtifactsBuilt() { const cachePath = path.join(__dirname, "../dist", "intermediates", "macos", "CMakeCache.txt"); const sourceInputs = [ nativeScriptSourceRoot, + path.join(metadataGeneratorRoot, "src"), + path.join(metadataGeneratorRoot, "include"), + path.join(metadataGeneratorRoot, "CMakeLists.txt"), + metadataGeneratorBinary, + metadataGeneratorBuildStepScript, + path.join(__dirname, "build_metadata_generator.sh"), path.join(__dirname, "build_nativescript.sh") ]; @@ -478,20 +498,29 @@ function ensureMacOSRuntimeArtifactsBuilt() { ); const artifactMtime = getPathStats(nativeScriptXCFramework).maxMtimeMs; let configuredEngine = null; + let configuredFfiBackend = null; if (fs.existsSync(cachePath)) { try { const cache = fs.readFileSync(cachePath, "utf8"); - const match = cache.match(/^TARGET_ENGINE:STRING=(.+)$/m); - if (match) { - configuredEngine = match[1].trim().toLowerCase(); + const engineMatch = cache.match(/^TARGET_ENGINE:STRING=(.+)$/m); + if (engineMatch) { + configuredEngine = engineMatch[1].trim().toLowerCase(); + } + const ffiBackendMatch = cache.match(/^NS_FFI_BACKEND:STRING=(.+)$/m); + if (ffiBackendMatch) { + configuredFfiBackend = ffiBackendMatch[1].trim().toLowerCase(); } } catch (_) { configuredEngine = null; + configuredFfiBackend = null; } } - if (artifactMtime > 0 && artifactMtime >= sourceMtime && configuredEngine === requestedEngine) { + if (artifactMtime > 0 && + artifactMtime >= sourceMtime && + configuredEngine === requestedEngine && + configuredFfiBackend === requestedFfiBackend) { return; } @@ -500,10 +529,15 @@ function ensureMacOSRuntimeArtifactsBuilt() { throw new Error(`Unsupported MACOS_TEST_ENGINE: ${requestedEngine}`); } - console.log(`NativeScript macOS artifacts are missing, stale, or built for '${configuredEngine ?? "unknown"}'; running ${requestedEngine} build...`); + const supportedFfiBackends = new Set(["auto", "napi", "direct"]); + if (!supportedFfiBackends.has(requestedFfiBackend)) { + throw new Error(`Unsupported MACOS_TEST_FFI_BACKEND: ${requestedFfiBackend}`); + } + + console.log(`NativeScript macOS artifacts are missing, stale, or built for '${configuredEngine ?? "unknown"}/${configuredFfiBackend ?? "unknown"}'; running ${requestedEngine}/${requestedFfiBackend} build...`); runBuildAndRequireSuccess( path.join(__dirname, "build_nativescript.sh"), - ["--macos", "--no-iphone", "--no-simulator", `--${requestedEngine}`], + ["--macos", "--no-iphone", "--no-simulator", `--${requestedEngine}`, `--ffi-backend=${requestedFfiBackend}`], commandTimeoutMs ); } @@ -526,7 +560,7 @@ function buildTestRunnerApp() { ensureMetadataGeneratorBuilt(); ensureMacOSRuntimeArtifactsBuilt(); - const nativeFingerprint = `${requestedEngine}:${createBuildFingerprint(macosBuildInputs)}`; + const nativeFingerprint = `${requestedEngine}:${requestedFfiBackend}:${createBuildFingerprint(macosBuildInputs)}`; const existingBuildState = readBuildState(); const canReuseBuild = process.env.MACOS_TEST_CLEAN_BUILD !== "1" && fs.existsSync(appPath) && @@ -599,9 +633,15 @@ function main() { } const runArgs = ["-logjunit"]; + if (verboseSpecs) { + runArgs.push("-verbose-specs"); + } if (requestedTests.length > 0) { runArgs.push("-tests", requestedTests); } + if (requestedSpecs.length > 0) { + runArgs.push("-specs", requestedSpecs); + } console.log(`Launching app and streaming logs: ${appBinaryPath} ${runArgs.join(" ")}`); diff --git a/scripts/test_react_native_ffi_compat.sh b/scripts/test_react_native_ffi_compat.sh new file mode 100755 index 00000000..b69f6309 --- /dev/null +++ b/scripts/test_react_native_ffi_compat.sh @@ -0,0 +1,233 @@ +#!/bin/bash +set -euo pipefail +source "$(dirname "$0")/build_utils.sh" +source "$SCRIPT_DIR/react_native_app_utils.sh" + +RN_VERSION=${RN_FFI_COMPAT_VERSION:-0.85.3} +RN_CLI_VERSION=${RN_FFI_COMPAT_CLI_VERSION:-20.1.3} +APP_NAME=${RN_FFI_COMPAT_APP_NAME:-NativeScriptNativeApiFfiCompat} +APP_ROOT=${RN_FFI_COMPAT_APP_ROOT:-"$REPO_ROOT/build/react-native-ffi-compat"} +APP_DIR="$APP_ROOT/$APP_NAME" +CONFIGURATION=${IOS_CONFIGURATION:-Release} +FORCE_RECREATE=${RN_FFI_COMPAT_FORCE_RECREATE:-1} +BUILD_TIMEOUT_SECONDS=${RN_FFI_COMPAT_BUILD_TIMEOUT_SECONDS:-1800} +LAUNCH_TIMEOUT_SECONDS=${RN_FFI_COMPAT_LAUNCH_TIMEOUT_SECONDS:-120} +BUNDLE_ID="org.reactjs.native.example.$APP_NAME" +MARKER="NATIVESCRIPT_RN_FFI_COMPAT" +MARKER_FILE_NAME="NativeScriptNativeApiSmoke.marker" +APP_TSX="$REPO_ROOT/test/react-native/ffi-compat/App.tsx" +RUNTIME_TESTS_SOURCE="$REPO_ROOT/test/runtime/runner/app/tests" +FIXTURES_SOURCE="$REPO_ROOT/test/runtime/fixtures" +GENERATED_METADATA_DIR="$APP_ROOT/metadata" + +function rn_generate_ffi_test_metadata() { + local output_dir="$1" + local generator_arch + local generator + local sdk_root + local sdk_version + + generator_arch=$(uname -m) + generator="$REPO_ROOT/metadata-generator/dist/$generator_arch/bin/objc-metadata-generator" + if [[ ! -x "$generator" ]]; then + generator="$REPO_ROOT/metadata-generator/dist/arm64/bin/objc-metadata-generator" + fi + if [[ ! -x "$generator" ]]; then + "$SCRIPT_DIR/build_metadata_generator.sh" + fi + if [[ ! -x "$generator" ]]; then + echo "Metadata generator not found at $generator" >&2 + exit 1 + fi + + sdk_root=$(xcrun --sdk iphonesimulator --show-sdk-path) + sdk_version=$(xcrun --sdk iphonesimulator --show-sdk-version) + + rm -rf "$output_dir" + mkdir -p "$output_dir" + + for arch in arm64 x86_64; do + checkpoint "Generating RN FFI fixture metadata for $arch..." + "$generator" \ + -verbose \ + -output-bin "$output_dir/metadata.ios-sim.$arch.nsmd" \ + -output-umbrella "$output_dir/umbrella-$arch.h" \ + Xclang \ + -isysroot "$sdk_root" \ + -std=gnu17 \ + -target "$arch-apple-ios$sdk_version-simulator" \ + -I"$FIXTURES_SOURCE" \ + -fmodules \ + -fmodule-map-file="$FIXTURES_SOURCE/module.modulemap" \ + -DDEBUG=1 \ + > "$output_dir/metagen-$arch.log" 2>&1 + done +} + +function rn_install_ffi_runtime_specs() { + local tests_destination="$APP_DIR/ns-runtime-tests" + rm -rf "$tests_destination" + mkdir -p \ + "$tests_destination/Infrastructure" \ + "$tests_destination/Marshalling/Primitives" \ + "$tests_destination/Marshalling" + + cp "$RUNTIME_TESTS_SOURCE/Infrastructure/utf8.js" "$tests_destination/Infrastructure/" + cp "$RUNTIME_TESTS_SOURCE/FunctionsTests.js" "$tests_destination/" + cp "$RUNTIME_TESTS_SOURCE/MethodCallsTests.js" "$tests_destination/" + cp "$RUNTIME_TESTS_SOURCE/Marshalling/Primitives/Function.js" "$tests_destination/Marshalling/Primitives/" + cp "$RUNTIME_TESTS_SOURCE/Marshalling/Primitives/Static.js" "$tests_destination/Marshalling/Primitives/" + cp "$RUNTIME_TESTS_SOURCE/Marshalling/Primitives/Instance.js" "$tests_destination/Marshalling/Primitives/" + cp "$RUNTIME_TESTS_SOURCE/Marshalling/Primitives/Derived.js" "$tests_destination/Marshalling/Primitives/" + cp "$RUNTIME_TESTS_SOURCE/Marshalling/ObjCTypesTests.js" "$tests_destination/Marshalling/" + cp "$RUNTIME_TESTS_SOURCE/Marshalling/ConstantsTests.js" "$tests_destination/Marshalling/" + cp "$RUNTIME_TESTS_SOURCE/Marshalling/RecordTests.js" "$tests_destination/Marshalling/" + cp "$RUNTIME_TESTS_SOURCE/Marshalling/VectorTests.js" "$tests_destination/Marshalling/" + cp "$RUNTIME_TESTS_SOURCE/Marshalling/NSStringTests.js" "$tests_destination/Marshalling/" + cp "$RUNTIME_TESTS_SOURCE/Marshalling/PointerTests.js" "$tests_destination/Marshalling/" + cp "$RUNTIME_TESTS_SOURCE/Marshalling/ReferenceTests.js" "$tests_destination/Marshalling/" + cp "$RUNTIME_TESTS_SOURCE/Marshalling/FunctionPointerTests.js" "$tests_destination/Marshalling/" + cp "$RUNTIME_TESTS_SOURCE/Marshalling/EnumTests.js" "$tests_destination/Marshalling/" + cp "$RUNTIME_TESTS_SOURCE/Marshalling/ProtocolTests.js" "$tests_destination/Marshalling/" +} + +function rn_install_ffi_test_fixtures_pod() { + local pod_dir="$APP_DIR/ios/NativeScriptFfiTestFixtures" + local podspec="$pod_dir/NativeScriptFfiTestFixtures.podspec" + local podfile="$APP_DIR/ios/Podfile" + + rm -rf "$pod_dir" + mkdir -p "$pod_dir" + rsync -a --delete "$FIXTURES_SOURCE/" "$pod_dir/fixtures/" + + cat > "$podspec" <<'RUBY' +fixture_keepalive_ldflags = if File.exist?(File.join(__dir__, "fixtures/exported-symbols.txt")) + File.readlines(File.join(__dir__, "fixtures/exported-symbols.txt"), chomp: true) + .map(&:strip) + .reject(&:empty?) + .map { |symbol| "-Wl,-u,#{symbol}" } + .join(" ") +else + "" +end + +Pod::Spec.new do |s| + s.name = "NativeScriptFfiTestFixtures" + s.version = "0.0.1" + s.summary = "NativeScript FFI runtime test fixtures for React Native compatibility tests" + s.homepage = "https://github.com/NativeScript/napi-ios" + s.license = "Apache-2.0" + s.author = "NativeScript Team" + s.platforms = { :ios => "13.0" } + s.source = { :path => "." } + s.requires_arc = true + s.prefix_header_file = "fixtures/TestFixtures-Prefix.h" + s.source_files = "fixtures/**/*.{h,m}" + s.public_header_files = "fixtures/**/*.h" + s.frameworks = "Foundation", "UIKit", "CoreGraphics", "SceneKit" + s.compiler_flags = "-Wno-return-stack-address -Wno-strict-prototypes" + s.pod_target_xcconfig = { + "HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/fixtures\"", + "CLANG_ENABLE_MODULES" => "YES", + } + s.user_target_xcconfig = { + "OTHER_LDFLAGS" => "$(inherited) -ObjC -force_load $(PODS_CONFIGURATION_BUILD_DIR)/NativeScriptFfiTestFixtures/libNativeScriptFfiTestFixtures.a #{fixture_keepalive_ldflags}", + } +end +RUBY + + if ! grep -q "NativeScriptFfiTestFixtures" "$podfile"; then + perl -0pi -e "s/(target '$APP_NAME' do\\n)/\\1 pod 'NativeScriptFfiTestFixtures', :path => '.\\/NativeScriptFfiTestFixtures'\\n/" "$podfile" + fi +} + +function rn_override_turbo_metadata_for_ffi_tests() { + local package_metadata_dir="$APP_DIR/node_modules/@nativescript/react-native/metadata" + rm -f "$package_metadata_dir/metadata.ios-sim.arm64.nsmd" \ + "$package_metadata_dir/metadata.ios-sim.x86_64.nsmd" + cp "$GENERATED_METADATA_DIR/metadata.ios-sim.arm64.nsmd" "$package_metadata_dir/" + cp "$GENERATED_METADATA_DIR/metadata.ios-sim.x86_64.nsmd" "$package_metadata_dir/" +} + +rn_build_turbo_tarball +TARBALL=$(rn_latest_turbo_tarball) + +if [[ "$FORCE_RECREATE" == "1" ]]; then + rm -rf "$APP_DIR" +fi + +rn_create_app_if_missing "$APP_DIR" "$APP_ROOT" "$APP_NAME" "$RN_VERSION" "$RN_CLI_VERSION" "React Native FFI compatibility app" +rn_install_turbo_tarball "$APP_DIR" "$TARBALL" "FFI compatibility app" + +checkpoint "Installing FFI compatibility entrypoint..." +cp "$APP_TSX" "$APP_DIR/App.tsx" +rn_install_ffi_runtime_specs +rn_generate_ffi_test_metadata "$GENERATED_METADATA_DIR" +rn_override_turbo_metadata_for_ffi_tests +rn_install_ffi_test_fixtures_pod + +rn_install_pods "$APP_DIR" "FFI compatibility app" +UDID=$(rn_require_ios_simulator) +rn_build_ios_app "$APP_DIR" "$APP_ROOT" "$APP_NAME" "$CONFIGURATION" "$UDID" "$BUILD_TIMEOUT_SECONDS" "FFI compatibility app" +APP_BUNDLE="$RN_APP_BUNDLE" + +checkpoint "Launching FFI compatibility app and waiting for test marker..." +MARKER_FILE=$(rn_launch_app_with_marker "$UDID" "$APP_BUNDLE" "$BUNDLE_ID" "$MARKER_FILE_NAME") + +node - "$MARKER_FILE" "$MARKER" "$LAUNCH_TIMEOUT_SECONDS" <<'NODE' +const fs = require('fs'); +const [markerFile, marker, timeoutSecondsText] = process.argv.slice(2); +const timeoutMs = Number(timeoutSecondsText) * 1000; +const startedAt = Date.now(); +let lastContent = ''; +let lastPayload = null; +let lastStage = ''; + +function finish(payload) { + console.log(`${marker} ${JSON.stringify({markerFile, payload})}`); + if (!payload || payload.status !== 'pass') { + process.exit(1); + } + process.exit(0); +} + +function poll() { + if (fs.existsSync(markerFile)) { + const content = fs.readFileSync(markerFile, 'utf8').trim(); + if (content && content !== lastContent) { + lastContent = content; + try { + lastPayload = JSON.parse(content); + } catch (error) { + if (content.startsWith('stage=')) { + lastStage = content; + console.log(`${marker} ${JSON.stringify({markerFile, stage: content.slice('stage='.length)})}`); + return; + } + console.error(`Invalid ${marker} marker content at ${markerFile}: ${content}`); + process.exit(1); + } + console.log(`${marker} ${JSON.stringify({markerFile, payload: lastPayload})}`); + if (lastPayload.status !== 'running') { + finish(lastPayload); + } + } + } + + if (Date.now() - startedAt > timeoutMs) { + console.error(`Timed out waiting for ${marker} file at ${markerFile}.`); + if (lastPayload) { + console.error(`Last ${marker} payload: ${JSON.stringify(lastPayload)}`); + } else if (lastStage) { + console.error(`Last ${marker} native stage: ${lastStage}`); + } + process.exit(1); + } + + setTimeout(poll, 2000); +} + +poll(); +NODE + +checkpoint "React Native NativeScript FFI compatibility suite passed." diff --git a/scripts/test_react_native_turbomodule.sh b/scripts/test_react_native_turbomodule.sh new file mode 100755 index 00000000..bd1cdd0b --- /dev/null +++ b/scripts/test_react_native_turbomodule.sh @@ -0,0 +1,121 @@ +#!/bin/bash +set -euo pipefail +source "$(dirname "$0")/build_utils.sh" +source "$SCRIPT_DIR/react_native_app_utils.sh" + +RN_VERSION=${RN_VERSION:-0.85.3} +RN_CLI_VERSION=${RN_CLI_VERSION:-20.1.3} +APP_NAME=${RN_SMOKE_APP_NAME:-NativeScriptNativeApiSmoke} +APP_ROOT=${RN_SMOKE_APP_ROOT:-"$REPO_ROOT/build/react-native-smoke"} +APP_DIR="$APP_ROOT/$APP_NAME" +CONFIGURATION=${IOS_CONFIGURATION:-Release} +FORCE_RECREATE=${RN_SMOKE_FORCE_RECREATE:-1} +BUILD_TIMEOUT_SECONDS=${RN_SMOKE_BUILD_TIMEOUT_SECONDS:-1800} +LAUNCH_TIMEOUT_SECONDS=${RN_SMOKE_LAUNCH_TIMEOUT_SECONDS:-90} +MARKER="NATIVESCRIPT_RN_TURBO_SMOKE_PASS" +BUNDLE_ID="org.reactjs.native.example.$APP_NAME" +MARKER_FILE_NAME="NativeScriptNativeApiSmoke.marker" + +rn_build_turbo_tarball +TARBALL=$(rn_latest_turbo_tarball) + +if [[ "$FORCE_RECREATE" == "1" ]]; then + rm -rf "$APP_DIR" +fi + +rn_create_app_if_missing "$APP_DIR" "$APP_ROOT" "$APP_NAME" "$RN_VERSION" "$RN_CLI_VERSION" "React Native smoke app" +rn_install_turbo_tarball "$APP_DIR" "$TARBALL" "smoke app" + +checkpoint "Writing smoke app entrypoint..." +node - "$APP_DIR/App.tsx" <<'NODE' +const fs = require('fs'); +const target = process.argv[2]; + +fs.writeFileSync(target, `import React from 'react'; +import {useEffect, useState} from 'react'; +import {SafeAreaView, Text} from 'react-native'; +import NativeScript from '@nativescript/react-native'; + +const marker = 'NATIVESCRIPT_RN_TURBO_SMOKE_PASS'; + +async function runSmoke(): Promise { + try { + const installed = NativeScript.init(); + const api = (globalThis as any).__nativeScriptNativeApi; + if (!installed || !api) { + throw new Error('NativeScript Native API JSI host object was not installed'); + } + + const nsObject = NSObject; + if (!nsObject || typeof nsObject.alloc !== 'function') { + throw new Error('NSObject global install failed'); + } + + if (NSURLErrorTimedOut !== -1001) { + throw new Error('constant global install failed'); + } + + if (NSComparisonResult.Same !== 0 || UIUserInterfaceStyle.Dark !== 2) { + throw new Error('enum global install failed'); + } + + let nativeCallsRanOnMainThread = false; + await NativeScript.runOnUI(() => { + nativeCallsRanOnMainThread = NSThread?.isMainThread === true; + if (!nativeCallsRanOnMainThread) { + throw new Error('runOnUI did not dispatch native calls to the main thread'); + } + }); + + const summary = { + installed, + nativeCallsRanOnMainThread, + runtime: api.runtime, + backend: api.backend, + classes: api.metadata?.classes ?? 0, + constants: api.metadata?.constants ?? 0, + enums: api.metadata?.enums ?? 0, + constant: NSURLErrorTimedOut, + enumValue: UIUserInterfaceStyle.Dark, + metadataPath: NativeScript.defaultMetadataPath(), + turboBackend: NativeScript.getRuntimeBackend(), + }; + + console.log(marker + ' ' + JSON.stringify(summary)); + return JSON.stringify(summary, null, 2); + } catch (error) { + console.error('NATIVESCRIPT_RN_TURBO_SMOKE_FAIL', error); + throw error; + } +} + +export default function App(): React.JSX.Element { + const [result, setResult] = useState('Running NativeScript TurboModule smoke test...'); + + useEffect(() => { + runSmoke() + .then(setResult) + .catch((error) => { + setResult(error instanceof Error ? error.message : String(error)); + }); + }, []); + + return ( + + {result} + + ); +} +`); +NODE + +rn_install_pods "$APP_DIR" "smoke app" +UDID=$(rn_require_ios_simulator) +rn_build_ios_app "$APP_DIR" "$APP_ROOT" "$APP_NAME" "$CONFIGURATION" "$UDID" "$BUILD_TIMEOUT_SECONDS" "smoke app" +APP_BUNDLE="$RN_APP_BUNDLE" + +checkpoint "Launching smoke app and waiting for TurboModule marker..." +MARKER_FILE=$(rn_launch_app_with_marker "$UDID" "$APP_BUNDLE" "$BUNDLE_ID" "$MARKER_FILE_NAME") +rn_wait_for_marker_file "$MARKER_FILE" "$MARKER" "$LAUNCH_TIMEOUT_SECONDS" + +checkpoint "React Native NativeScript TurboModule smoke test passed." diff --git a/test/react-native/ffi-compat/App.tsx b/test/react-native/ffi-compat/App.tsx new file mode 100644 index 00000000..9cf0b687 --- /dev/null +++ b/test/react-native/ffi-compat/App.tsx @@ -0,0 +1,970 @@ +import React, {useEffect, useState} from 'react'; +import {SafeAreaView, ScrollView, Text} from 'react-native'; +import NativeScript, {defineUIKitView} from '@nativescript/react-native'; +import NativeScriptNativeApi from '@nativescript/react-native/src/NativeScriptNativeApi'; + +declare const require: any; + +type TestStatus = 'pass' | 'fail' | 'skip'; + +type TestCase = { + name: string; + run: () => void | Promise; +}; + +type TestResult = { + name: string; + status: TestStatus; + error?: string; +}; + +type RuntimeSpec = { + name: string; + run: Function; + beforeEach: Function[]; + afterEach: Function[]; +}; + +type RuntimeSuite = { + name: string; + beforeEach: Function[]; + afterEach: Function[]; +}; + +type RuntimeSpecRegistry = { + specs: RuntimeSpec[]; + skipped: TestResult[]; +}; + +const marker = 'NATIVESCRIPT_RN_FFI_COMPAT'; +const runtimeSpecTimeoutMs = 15000; +const runtimeFailureLimit = 25; +let currentStep = 'startup'; +let lastGlobalAccess = ''; +let activeAsyncReject: ((reason?: unknown) => void) | null = null; +const uikitPluginIdentifier = 'NativeScriptUIKitPluginView'; +const uikitPluginLabelTag = 101; + +class PendingSpecError extends Error { + constructor(message = 'Pending') { + super(message); + this.name = 'PendingSpecError'; + } +} + +function g(name: string): any { + lastGlobalAccess = name; + return (globalThis as Record)[name]; +} + +function step(name: string, callback: () => T): T { + currentStep = name; + return callback(); +} + +function assert(condition: unknown, message: string): asserts condition { + if (!condition) { + throw new Error(message); + } +} + +function assertEqual(actual: T, expected: T, message: string) { + if (!Object.is(actual, expected)) { + throw new Error(`${message}: expected ${String(expected)}, got ${String(actual)}`); + } +} + +function assertClose(actual: number, expected: number, message: string) { + if (Math.abs(actual - expected) > 0.0001) { + throw new Error(`${message}: expected ${expected}, got ${actual}`); + } +} + +function ptrNumber(value: any): number { + assert(value && typeof value.toNumber === 'function', 'expected Pointer value'); + return value.toNumber(); +} + +function sameNativeHandle(a: any, b: any): boolean { + const interop = g('interop'); + return ptrNumber(interop.handleof(a)) === ptrNumber(interop.handleof(b)); +} + +function stringify(value: unknown): string { + if (typeof value === 'string') { + return JSON.stringify(value); + } + if (typeof value === 'function') { + return `[Function ${value.name || 'anonymous'}]`; + } + try { + return JSON.stringify(value); + } catch { + return String(value); + } +} + +function fail(message: string): never { + throw new Error(message); +} + +function writeMarker(payload: unknown) { + const content = JSON.stringify(payload, null, 2); + const writer = (NativeScriptNativeApi as any).__writeTestMarker; + if (typeof writer === 'function') { + writer(content); + } + console.log(`${marker} ${content}`); +} + +function waitFor( + read: () => T | undefined | null | false, + message: string, + timeoutMs = 5000, +): Promise { + const startedAt = Date.now(); + return new Promise((resolve, reject) => { + function poll() { + const value = read(); + if (value) { + resolve(value); + return; + } + if (Date.now() - startedAt > timeoutMs) { + reject(new Error(message)); + return; + } + setTimeout(poll, 50); + } + poll(); + }); +} + +function waitForAsync( + read: () => Promise, + message: string, + timeoutMs = 5000, +): Promise { + const startedAt = Date.now(); + return new Promise((resolve, reject) => { + async function poll() { + const value = await read(); + if (value) { + resolve(value); + return; + } + if (Date.now() - startedAt > timeoutMs) { + reject(new Error(message)); + return; + } + setTimeout(poll, 50); + } + poll().catch(reject); + }); +} + +function isAsymmetricAny(value: unknown): value is {expectedType: Function} { + return ( + Boolean(value) && + typeof value === 'object' && + (value as Record).__nativeScriptJasmineAny === true && + typeof (value as Record).expectedType === 'function' + ); +} + +function matchesAny(actual: unknown, expectedType: Function): boolean { + if (expectedType === String) { + return typeof actual === 'string' || actual instanceof String; + } + if (expectedType === Number) { + return typeof actual === 'number' || actual instanceof Number; + } + if (expectedType === Boolean) { + return typeof actual === 'boolean' || actual instanceof Boolean; + } + if (expectedType === Function) { + return typeof actual === 'function'; + } + return actual instanceof (expectedType as any); +} + +function deepEqual(actual: unknown, expected: unknown, seen = new Set()): boolean { + if (isAsymmetricAny(expected)) { + return matchesAny(actual, expected.expectedType); + } + if (Object.is(actual, expected)) { + return true; + } + if (actual instanceof Date && expected instanceof Date) { + return actual.getTime() === expected.getTime(); + } + if (actual instanceof ArrayBuffer && expected instanceof ArrayBuffer) { + if (actual.byteLength !== expected.byteLength) { + return false; + } + const actualBytes = new Uint8Array(actual); + const expectedBytes = new Uint8Array(expected); + return actualBytes.every((value, index) => value === expectedBytes[index]); + } + if (ArrayBuffer.isView(actual as any) && ArrayBuffer.isView(expected as any)) { + const actualView = actual as ArrayLike; + const expectedView = expected as ArrayLike; + if (actualView.length !== expectedView.length) { + return false; + } + for (let i = 0; i < actualView.length; i++) { + if (!deepEqual(actualView[i], expectedView[i], seen)) { + return false; + } + } + return true; + } + if ( + actual == null || + expected == null || + typeof actual !== 'object' || + typeof expected !== 'object' + ) { + return false; + } + if (seen.has(actual)) { + return true; + } + seen.add(actual); + const actualKeys = Object.keys(actual as Record); + const expectedKeys = Object.keys(expected as Record); + if (actualKeys.length !== expectedKeys.length) { + return false; + } + for (const key of expectedKeys) { + if (!Object.prototype.hasOwnProperty.call(actual, key)) { + return false; + } + if ( + !deepEqual( + (actual as Record)[key], + (expected as Record)[key], + seen, + ) + ) { + return false; + } + } + return true; +} + +function installRuntimeSpecGlobals(): RuntimeSpecRegistry { + const registry: RuntimeSpecRegistry = {specs: [], skipped: []}; + const rootSuite: RuntimeSuite = {name: 'runtime ffi', beforeEach: [], afterEach: []}; + const suiteStack: RuntimeSuite[] = [rootSuite]; + const globalObject = globalThis as Record; + const originalSetTimeout = globalObject.setTimeout; + + globalObject.global = globalThis; + globalObject.isSimulator = true; + globalObject.__runtimeVersion = {major: 999, minor: 0, patch: 0}; + globalObject.process = { + ...(globalObject.process ?? {}), + versions: { + ...(globalObject.process?.versions ?? {}), + engine: 'hermes', + }, + }; + if (typeof globalObject.gc !== 'function') { + globalObject.gc = () => undefined; + } + globalObject.utf8 = require('./ns-runtime-tests/Infrastructure/utf8'); + globalObject.UNUSED = function (_param: unknown) { + return undefined; + }; + + globalObject.setTimeout = (callback: Function, timeout?: number, ...args: unknown[]) => + originalSetTimeout( + (...callbackArgs: unknown[]) => { + try { + callback(...callbackArgs); + } catch (error) { + if (activeAsyncReject) { + activeAsyncReject(error); + return; + } + throw error; + } + }, + timeout, + ...args, + ); + + globalObject.describe = (name: string, body: Function) => { + const suite: RuntimeSuite = {name: String(name), beforeEach: [], afterEach: []}; + suiteStack.push(suite); + try { + body(); + } finally { + suiteStack.pop(); + } + }; + + const skipReasonForRuntimeSpec = ( + specName: string, + body: Function, + ): string | undefined => { + let source = ''; + try { + source = Function.prototype.toString.call(body); + } catch { + source = ''; + } + + if (source.includes('interop.addMethod')) { + return 'Requires interop.addMethod; excluded from the RN direct-JSI FFI slice until the explicit decorator hook is implemented.'; + } + return undefined; + }; + + globalObject.it = (name: string, body: Function) => { + const suites = suiteStack.slice(1); + const specName = `${suites.map((suite) => suite.name).join(' > ')} > ${name}`; + const skipReason = skipReasonForRuntimeSpec(specName, body); + if (skipReason) { + registry.skipped.push({ + name: specName, + status: 'skip', + error: skipReason, + }); + return; + } + registry.specs.push({ + name: specName, + run: body, + beforeEach: suiteStack.flatMap((suite) => suite.beforeEach), + afterEach: suiteStack + .slice() + .reverse() + .flatMap((suite) => suite.afterEach), + }); + }; + + globalObject.xit = (name: string) => { + const suites = suiteStack.slice(1); + registry.skipped.push({ + name: `${suites.map((suite) => suite.name).join(' > ')} > ${name}`, + status: 'skip', + error: 'Disabled with xit', + }); + }; + globalObject.fit = globalObject.it; + + globalObject.beforeEach = (body: Function) => { + suiteStack[suiteStack.length - 1].beforeEach.push(body); + }; + globalObject.afterEach = (body: Function) => { + suiteStack[suiteStack.length - 1].afterEach.push(body); + }; + globalObject.pending = (reason?: string) => { + throw new PendingSpecError(reason || 'Pending'); + }; + globalObject.jasmine = { + any(expectedType: Function) { + return {__nativeScriptJasmineAny: true, expectedType}; + }, + }; + + globalObject.expect = (actual: unknown) => { + const makeMatchers = (negated: boolean) => { + const check = (condition: boolean, message: string) => { + const passed = negated ? !condition : condition; + if (!passed) { + fail(negated ? `Expected not: ${message}` : message); + } + }; + + return { + toBe(expected: unknown, message?: string) { + check( + Object.is(actual, expected), + message || `expected ${stringify(actual)} to be ${stringify(expected)}`, + ); + }, + toEqual(expected: unknown, message?: string) { + check( + deepEqual(actual, expected), + message || `expected ${stringify(actual)} to equal ${stringify(expected)}`, + ); + }, + toBeDefined(message?: string) { + check(actual !== undefined, message || `expected ${stringify(actual)} to be defined`); + }, + toBeUndefined(message?: string) { + check(actual === undefined, message || `expected ${stringify(actual)} to be undefined`); + }, + toBeNull(message?: string) { + check(actual === null, message || `expected ${stringify(actual)} to be null`); + }, + toBeTruthy(message?: string) { + check(Boolean(actual), message || `expected ${stringify(actual)} to be truthy`); + }, + toBeGreaterThan(expected: number, message?: string) { + check( + typeof actual === 'number' && actual > expected, + message || `expected ${stringify(actual)} to be greater than ${expected}`, + ); + }, + toContain(expected: unknown, message?: string) { + check( + typeof actual === 'string' + ? actual.includes(String(expected)) + : Array.isArray(actual) && actual.includes(expected), + message || `expected ${stringify(actual)} to contain ${stringify(expected)}`, + ); + }, + toMatch(expected: RegExp | string, message?: string) { + const text = String(actual); + const matched = + expected instanceof RegExp ? expected.test(text) : text.includes(String(expected)); + check(matched, message || `expected ${text} to match ${String(expected)}`); + }, + toBeCloseTo(expected: number, precision = 2, message?: string) { + const tolerance = Math.pow(10, -precision) / 2; + check( + typeof actual === 'number' && Math.abs(actual - expected) < tolerance, + message || `expected ${stringify(actual)} to be close to ${expected}`, + ); + }, + toThrow(message?: string) { + checkThrows(actual, undefined, message, check); + }, + toThrowError(expected?: RegExp | string | Function, message?: string) { + checkThrows(actual, expected, message, check); + }, + }; + }; + + const matchers: any = makeMatchers(false); + matchers.not = makeMatchers(true); + return matchers; + }; + + return registry; +} + +function checkThrows( + actual: unknown, + expected: RegExp | string | Function | undefined, + message: string | undefined, + check: (condition: boolean, message: string) => void, +) { + if (typeof actual !== 'function') { + check(false, message || 'expected value to be a function that throws'); + return; + } + + let thrown: unknown; + try { + actual(); + } catch (error) { + thrown = error; + } + + if (thrown === undefined) { + check(false, message || 'expected function to throw'); + return; + } + + if (expected === undefined) { + check(true, ''); + return; + } + + const thrownMessage = thrown instanceof Error ? thrown.message : String(thrown); + if (expected instanceof RegExp) { + check( + expected.test(thrownMessage), + message || `expected thrown error ${thrownMessage} to match ${expected}`, + ); + } else if (typeof expected === 'string') { + check( + thrownMessage.includes(expected), + message || `expected thrown error ${thrownMessage} to include ${expected}`, + ); + } else { + check(thrown instanceof (expected as any), message || 'expected thrown error type to match'); + } +} + +function loadRuntimeFfiSpecs() { + require('./ns-runtime-tests/FunctionsTests'); + require('./ns-runtime-tests/MethodCallsTests'); + require('./ns-runtime-tests/Marshalling/Primitives/Function'); + require('./ns-runtime-tests/Marshalling/Primitives/Static'); + require('./ns-runtime-tests/Marshalling/Primitives/Instance'); + require('./ns-runtime-tests/Marshalling/Primitives/Derived'); + require('./ns-runtime-tests/Marshalling/ObjCTypesTests'); + require('./ns-runtime-tests/Marshalling/ConstantsTests'); + require('./ns-runtime-tests/Marshalling/RecordTests'); + require('./ns-runtime-tests/Marshalling/VectorTests'); + require('./ns-runtime-tests/Marshalling/NSStringTests'); + require('./ns-runtime-tests/Marshalling/PointerTests'); + require('./ns-runtime-tests/Marshalling/ReferenceTests'); + require('./ns-runtime-tests/Marshalling/FunctionPointerTests'); + require('./ns-runtime-tests/Marshalling/EnumTests'); + require('./ns-runtime-tests/Marshalling/ProtocolTests'); +} + +async function runFunction(functionToRun: Function): Promise { + if (functionToRun.length > 0) { + await new Promise((resolve, reject) => { + let settled = false; + const timeout = setTimeout(() => { + if (!settled) { + settled = true; + reject(new Error(`Timed out after ${runtimeSpecTimeoutMs}ms`)); + } + }, runtimeSpecTimeoutMs); + activeAsyncReject = reject; + const done = (error?: unknown) => { + if (settled) { + return; + } + settled = true; + clearTimeout(timeout); + activeAsyncReject = null; + if (error) { + reject(error); + } else { + resolve(); + } + }; + (done as Record).fail = done; + try { + functionToRun(done); + } catch (error) { + done(error); + } + }); + return; + } + + const result = functionToRun(); + if (result && typeof result.then === 'function') { + await result; + } +} + +async function runRuntimeSpecs( + registry: RuntimeSpecRegistry, + progress: (current: string, results: TestResult[], total: number) => void, +): Promise { + const results: TestResult[] = [...registry.skipped]; + const total = registry.specs.length + registry.skipped.length; + + for (const spec of registry.specs) { + const startCount = results.length; + progress(spec.name, results, total); + + try { + for (const beforeEach of spec.beforeEach) { + await runFunction(beforeEach); + } + await runFunction(spec.run); + results.push({name: spec.name, status: 'pass'}); + } catch (error) { + if (error instanceof PendingSpecError) { + results.push({name: spec.name, status: 'skip', error: error.message}); + } else { + results.push({ + name: spec.name, + status: 'fail', + error: error instanceof Error ? `${error.name}: ${error.message}` : String(error), + }); + if (results.filter((result) => result.status === 'fail').length >= runtimeFailureLimit) { + break; + } + } + } finally { + activeAsyncReject = null; + for (const afterEach of spec.afterEach) { + try { + await runFunction(afterEach); + } catch (error) { + results.push({ + name: `${spec.name} cleanup`, + status: 'fail', + error: + error instanceof Error + ? `${error.name}: ${error.message}` + : String(error), + }); + } + } + } + + if (results.filter((result) => result.status === 'fail').length >= runtimeFailureLimit) { + break; + } + } + + return results; +} + +async function waitForUIKitPluginAttachment(): Promise { + await waitForAsync( + async () => { + let attached = false; + await NativeScript.runOnUI(() => { + const view = (globalThis as any).__nativeScriptUIKitPlugin?.view; + attached = Boolean(view?.superview && view?.window); + }); + return attached; + }, + 'JS-defined UIKit view was not attached to the RN tree', + ); +} + +const NativeScriptUIKitTestView = defineUIKitView<{ + title: string; + tint: 'blue' | 'green'; +}>({ + displayName: 'NativeScriptUIKitTestView', + create(props) { + const view = g('UIView').alloc().initWithFrame( + new (g('CGRect'))({ + origin: {x: 0, y: 0}, + size: {width: 160, height: 48}, + }), + ); + view.accessibilityIdentifier = uikitPluginIdentifier; + + const label = g('UILabel').alloc().initWithFrame( + new (g('CGRect'))({ + origin: {x: 8, y: 8}, + size: {width: 144, height: 32}, + }), + ); + label.tag = uikitPluginLabelTag; + label.textAlignment = g('NSTextAlignment').Center; + label.textColor = g('UIColor').whiteColor; + view.addSubview(label); + + (globalThis as any).__nativeScriptUIKitPlugin = { + created: true, + disposed: false, + title: '', + tint: props.tint, + view, + }; + return view; + }, + mounted(view, props) { + (globalThis as any).__nativeScriptUIKitPlugin.mounted = true; + (globalThis as any).__nativeScriptUIKitPlugin.nativeHandle = g( + 'interop', + ).handleof(view).address; + (globalThis as any).__nativeScriptUIKitPlugin.title = props.title; + }, + update(view, props) { + view.backgroundColor = + props.tint === 'green' ? g('UIColor').greenColor : g('UIColor').blueColor; + const label = view.viewWithTag(uikitPluginLabelTag); + label.text = props.title; + (globalThis as any).__nativeScriptUIKitPlugin.title = props.title; + (globalThis as any).__nativeScriptUIKitPlugin.tint = props.tint; + }, + dispose() { + const state = (globalThis as any).__nativeScriptUIKitPlugin; + if (state) { + state.disposed = true; + state.view = null; + } + }, +}); + +function buildReactNativeIntegrationTests(): TestCase[] { + return [ + { + name: 'RN host installs fixture-aware metadata-backed globals', + run() { + const api = g('__nativeScriptNativeApi'); + assert(api, 'Native API host object was not installed'); + assertEqual(api.runtime, 'jsi', 'runtime kind'); + assertEqual(api.backend, 'hermes', 'runtime backend'); + assert(api.metadata.classes > 0, 'class metadata should be loaded'); + assert(api.metadata.functions > 0, 'function metadata should be loaded'); + assert(api.metadata.constants > 0, 'constant metadata should be loaded'); + assert(api.metadata.enums > 0, 'enum metadata should be loaded'); + assert(api.metadata.protocols > 0, 'protocol metadata should be loaded'); + assert(api.metadata.structs > 0, 'struct metadata should be loaded'); + assert(typeof g('TNSBaseInterface').alloc === 'function', 'TNSBaseInterface missing'); + assert(typeof g('functionWithInt') === 'function', 'functionWithInt missing'); + assertEqual(g('TNSConstant'), 'TNSConstant', 'TNSConstant'); + }, + }, + { + name: 'dispatches Objective-C methods and properties with NativeScript-style calls', + run() { + const object = step('NSObject.alloc.init', () => g('NSObject').alloc().init()); + step('NSObject.respondsToSelector', () => + assert(object.respondsToSelector('init'), 'respondsToSelector failed'), + ); + step('NSObject.isKindOfClass', () => + assert(object.isKindOfClass(g('NSObject')), 'isKindOfClass failed'), + ); + step('NSObject.class', () => + assert(sameNativeHandle(object.class(), g('NSObject')), 'class() returned unexpected class'), + ); + + const array = step('NSMutableArray.alloc.init', () => + g('NSMutableArray').alloc().init(), + ); + step('NSMutableArray.addObject alpha', () => array.addObject('alpha')); + step('NSMutableArray.addObject beta', () => array.addObject('beta')); + step('NSMutableArray.count', () => assertEqual(array.count, 2, 'NSMutableArray count')); + step('NSMutableArray.objectAtIndex', () => + assertEqual(array.objectAtIndex(1), 'beta', 'NSMutableArray objectAtIndex'), + ); + }, + }, + { + name: 'marshals JavaScript arrays, objects, strings, and typed arrays into Foundation values', + run() { + const nsArray = g('NSArray').arrayWithArray(['a', 'b']); + assertEqual(nsArray.count, 2, 'NSArray arrayWithArray count'); + assertEqual(nsArray.objectAtIndex(0), 'a', 'NSArray first value'); + + const nsDict = g('NSDictionary').dictionaryWithDictionary({ + answer: 42, + label: 'native', + }); + assertEqual(nsDict.objectForKey('answer'), 42, 'NSDictionary numeric value'); + assertEqual(nsDict.objectForKey('label'), 'native', 'NSDictionary string value'); + + const bytes = new Uint8Array([65, 66, 67, 0]); + const data = g('NSData').dataWithBytesLength(bytes, bytes.byteLength); + const roundTrip = new Uint8Array(g('interop').bufferFromData(data)); + assertEqual(roundTrip[0], 65, 'NSData byte 0'); + assertEqual(roundTrip[1], 66, 'NSData byte 1'); + assertEqual(roundTrip[2], 67, 'NSData byte 2'); + }, + }, + { + name: 'invokes C function pointer callbacks on the native caller thread', + run() { + let callbackRan = false; + const result = g('functionWithSimpleFunctionPointerOnBackground')( + (nativeCallerWasMainThread: number) => { + assertEqual(nativeCallerWasMainThread, 0, 'callback native caller thread'); + assertEqual(g('NSThread').isMainThread, false, 'callback JS native calls thread'); + callbackRan = true; + return g('NSThread').isMainThread ? 1 : 0; + }, + ); + assertEqual(result, 0, 'background callback return'); + assert(callbackRan, 'background callback did not run'); + }, + }, + { + name: 'runs UIKit native calls through runOnUI main-thread dispatch', + async run() { + let mainThread = false; + await NativeScript.runOnUI(() => { + mainThread = g('NSThread').isMainThread === true; + const color = g('UIColor').colorWithRedGreenBlueAlpha(0.1, 0.2, 0.3, 1); + assert(color, 'UIColor construction failed on UI thread'); + }); + assert(mainThread, 'runOnUI did not execute native calls on main thread'); + }, + }, + { + name: 'mounts JS-defined UIKit views through the React Native host component', + async run() { + await waitFor( + () => + (globalThis as any).__nativeScriptUIKitPlugin?.mounted === true && + (globalThis as any).__nativeScriptUIKitPlugin?.title === + 'Initial UIKit title', + 'JS-defined UIKit view did not mount', + ); + + await waitForUIKitPluginAttachment(); + await NativeScript.runOnUI(() => { + const view = (globalThis as any).__nativeScriptUIKitPlugin?.view; + assert(view?.superview, 'JS-defined UIKit view has no host superview'); + assert(view?.window, 'JS-defined UIKit view has no window'); + const label = view.viewWithTag(uikitPluginLabelTag); + assertEqual(label.text, 'Initial UIKit title', 'initial UIKit label text'); + }); + + const setTitle = (globalThis as any).__setNativeScriptUIKitTitle; + const setTint = (globalThis as any).__setNativeScriptUIKitTint; + assert(typeof setTitle === 'function', 'UIKit title setter was not installed'); + assert(typeof setTint === 'function', 'UIKit tint setter was not installed'); + setTitle('Updated UIKit title'); + setTint('green'); + + await waitFor( + () => + (globalThis as any).__nativeScriptUIKitPlugin?.title === + 'Updated UIKit title' && + (globalThis as any).__nativeScriptUIKitPlugin?.tint === 'green', + 'JS-defined UIKit view did not receive prop updates', + ); + + await waitForUIKitPluginAttachment(); + await NativeScript.runOnUI(() => { + const view = (globalThis as any).__nativeScriptUIKitPlugin?.view; + assert(view?.superview, 'updated JS-defined UIKit view has no host superview'); + assert(view?.window, 'updated JS-defined UIKit view has no window'); + const label = view.viewWithTag(uikitPluginLabelTag); + assertEqual(label.text, 'Updated UIKit title', 'updated UIKit label text'); + }); + }, + }, + ]; +} + +function summarize(results: TestResult[]) { + return { + passed: results.filter((result) => result.status === 'pass').length, + failed: results.filter((result) => result.status === 'fail').length, + skipped: results.filter((result) => result.status === 'skip').length, + total: results.length, + }; +} + +async function runCompatibilitySuite() { + writeMarker({ + marker, + status: 'running', + current: 'before NativeScript.init', + passed: 0, + total: 0, + results: [], + }); + try { + NativeScript.init(); + } catch (error) { + const message = + error instanceof Error + ? `${error.name}: ${error.message}; step=${currentStep}; global=${lastGlobalAccess}; stack=${error.stack ?? ''}` + : `${String(error)}; step=${currentStep}; global=${lastGlobalAccess}`; + writeMarker({ + marker, + status: 'fail', + current: 'NativeScript.init', + passed: 0, + total: 0, + results: [], + failures: [{name: 'NativeScript.init', status: 'fail', error: message}], + }); + throw error; + } + + const registry = installRuntimeSpecGlobals(); + loadRuntimeFfiSpecs(); + const rnTests = buildReactNativeIntegrationTests(); + const total = registry.specs.length + registry.skipped.length + rnTests.length; + + writeMarker({ + marker, + status: 'running', + current: 'initialized', + passed: 0, + total, + runtimeSpecs: { + registered: registry.specs.length, + skipped: registry.skipped.length, + }, + backend: NativeScript.getRuntimeBackend(), + }); + + const runtimeResults = await runRuntimeSpecs(registry, (current, results) => { + const runtimeSummary = summarize(results); + writeMarker({ + marker, + status: 'running', + current, + passed: runtimeSummary.passed, + total, + runtime: runtimeSummary, + failures: results.filter((result) => result.status === 'fail').slice(0, 50), + backend: NativeScript.getRuntimeBackend(), + }); + }); + + const rnResults: TestResult[] = []; + if (!runtimeResults.some((result) => result.status === 'fail')) { + for (const test of rnTests) { + try { + writeMarker({ + marker, + status: 'running', + current: test.name, + passed: summarize(runtimeResults).passed + summarize(rnResults).passed, + total, + runtime: summarize(runtimeResults), + reactNative: summarize(rnResults), + backend: NativeScript.getRuntimeBackend(), + }); + await test.run(); + rnResults.push({name: test.name, status: 'pass'}); + } catch (error) { + rnResults.push({ + name: test.name, + status: 'fail', + error: + error instanceof Error + ? `${error.name}: ${error.message}; step=${currentStep}; global=${lastGlobalAccess}` + : `${String(error)}; step=${currentStep}; global=${lastGlobalAccess}`, + }); + break; + } + } + } + + const results = [...runtimeResults, ...rnResults]; + const failed = results.find((result) => result.status === 'fail'); + const payload = { + marker, + status: failed ? 'fail' : 'pass', + ...summarize(results), + total, + runtime: summarize(runtimeResults), + reactNative: summarize(rnResults), + failures: results.filter((result) => result.status === 'fail').slice(0, 50), + backend: NativeScript.getRuntimeBackend(), + }; + writeMarker(payload); + if (failed) { + throw new Error(failed.error); + } + return payload; +} + +export default function App(): React.JSX.Element { + const [text, setText] = useState('Running NativeScript RN FFI compatibility tests...'); + const [uikitTitle, setUIKitTitle] = useState('Initial UIKit title'); + const [uikitTint, setUIKitTint] = useState<'blue' | 'green'>('blue'); + + useEffect(() => { + (globalThis as any).__setNativeScriptUIKitTitle = setUIKitTitle; + (globalThis as any).__setNativeScriptUIKitTint = setUIKitTint; + runCompatibilitySuite() + .then((payload) => setText(JSON.stringify(payload, null, 2))) + .catch((error) => { + setText(error instanceof Error ? error.message : String(error)); + }); + }, []); + + return ( + + + {text} + + + + ); +} diff --git a/test/runtime/fixtures/Marshalling/TNSFunctionPointers.h b/test/runtime/fixtures/Marshalling/TNSFunctionPointers.h index 708ee3cd..2e770aeb 100644 --- a/test/runtime/fixtures/Marshalling/TNSFunctionPointers.h +++ b/test/runtime/fixtures/Marshalling/TNSFunctionPointers.h @@ -3,5 +3,6 @@ long long (*functionWhichReturnsSimpleFunctionPointer())(long long); void functionWithSimpleFunctionPointer(int (*f)(int)); +int functionWithSimpleFunctionPointerOnBackground(int (*f)(int)); void functionWithComplexFunctionPointer(TNSNestedStruct (*f)(char p1, short p2, int p3, long p4, long long p5, unsigned char p6, unsigned short p7, unsigned int p8, unsigned long p9, unsigned long long p10, float p11, double p12, SEL p13, Class p14, Protocol* p15, NSObject* p16, TNSNestedStruct p17)); void* functionReturningFunctionPtrAsVoidPtr(); diff --git a/test/runtime/fixtures/Marshalling/TNSFunctionPointers.m b/test/runtime/fixtures/Marshalling/TNSFunctionPointers.m index 9b4149b2..41cea1f4 100644 --- a/test/runtime/fixtures/Marshalling/TNSFunctionPointers.m +++ b/test/runtime/fixtures/Marshalling/TNSFunctionPointers.m @@ -13,6 +13,17 @@ void functionWithSimpleFunctionPointer(int (*f)(int)) { TNSLog([NSString stringWithFormat:@"%d", result]); } +int functionWithSimpleFunctionPointerOnBackground(int (*f)(int)) { + __block int result = -1; + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + result = f([NSThread isMainThread] ? 1 : 0); + dispatch_semaphore_signal(semaphore); + }); + dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); + return result; +} + void functionWithComplexFunctionPointer(TNSNestedStruct (*f)(char p1, short p2, int p3, long p4, long long p5, unsigned char p6, unsigned short p7, unsigned int p8, unsigned long p9, unsigned long long p10, float p11, double p12, SEL p13, Class p14, Protocol* p15, NSObject* p16, TNSNestedStruct p17)) { static NSObject* object = nil; diff --git a/test/runtime/fixtures/TestFixtures.h b/test/runtime/fixtures/TestFixtures.h index eee33d78..0bb6cf60 100644 --- a/test/runtime/fixtures/TestFixtures.h +++ b/test/runtime/fixtures/TestFixtures.h @@ -1,4 +1,5 @@ #import +#import #if TARGET_OS_OSX #import diff --git a/test/runtime/fixtures/exported-symbols.txt b/test/runtime/fixtures/exported-symbols.txt index c506cba1..83e6c908 100644 --- a/test/runtime/fixtures/exported-symbols.txt +++ b/test/runtime/fixtures/exported-symbols.txt @@ -43,6 +43,7 @@ _functionWithSelector _functionWithShort _functionWithShortPtr _functionWithSimpleFunctionPointer +_functionWithSimpleFunctionPointerOnBackground _functionWithStructPtr _functionWithUChar _functionWithUCharPtr diff --git a/test/runtime/runner/app/tests/ApiTests.js b/test/runtime/runner/app/tests/ApiTests.js index f5ca4fe1..a0c289c3 100644 --- a/test/runtime/runner/app/tests/ApiTests.js +++ b/test/runtime/runner/app/tests/ApiTests.js @@ -883,13 +883,20 @@ describe(module.id, function () { // expect(stack).toBe(expectedStack); // }); - // it("should allow calling callbacks from another thread", function () { - // var result = TNSTestNativeCallbacks.callOnThread(function() { - // return 'method called'; - // }); + it("should invoke block callbacks on the native caller thread", function () { + if (!global.process || + !global.process.versions || + global.process.versions.engine !== "hermes") { + pending("Same-thread callback dispatch currently requires the Hermes thread-safe runtime."); + } - // expect(result).toBe('method called'); - // }); + var jsThreadHash = String(NSThread.currentThread.hash); + var callbackThreadHash = TNSTestNativeCallbacks.callOnThread(function() { + return String(NSThread.currentThread.hash); + }); + + expect(callbackThreadHash).not.toBe(jsThreadHash); + }); it("Unimplemented properties from UIBarItem class should be provided by the inheritors", function () { if (hasGlobalSymbol("UIBarButtonItem") && hasGlobalSymbol("UITabBarItem")) { diff --git a/test/runtime/runner/app/tests/Infrastructure/timers.js b/test/runtime/runner/app/tests/Infrastructure/timers.js index 3164b7f8..52c8de56 100644 --- a/test/runtime/runner/app/tests/Infrastructure/timers.js +++ b/test/runtime/runner/app/tests/Infrastructure/timers.js @@ -1,5 +1,80 @@ // https://github.com/NativeScript/NativeScript/blob/master/tns-core-modules/timer/timer.ios.ts +if (typeof global.__ns__setTimeout === "function" && + typeof global.__ns__clearTimeout === "function") { + global.setTimeout = global.__ns__setTimeout; + global.clearTimeout = global.__ns__clearTimeout; + if (typeof global.__ns__setInterval === "function") { + global.setInterval = global.__ns__setInterval; + } + if (typeof global.__ns__clearInterval === "function") { + global.clearInterval = global.__ns__clearInterval; + } +} else if (global.__nativeScriptNativeApi && + typeof NSTimer !== "undefined" && + typeof NSTimer.scheduledTimerWithTimeIntervalRepeatsBlock === "function") { + var directTimeoutCallbacks = new Map(); + var directTimerId = 0; + + function createDirectTimerAndGetId(callback, milliseconds, shouldRepeat) { + directTimerId++; + var id = directTimerId; + var pair = { k: null, disposed: false }; + var timer = NSTimer.scheduledTimerWithTimeIntervalRepeatsBlock( + milliseconds / 1000, + shouldRepeat, + function () { + if (pair.disposed) { + return; + } + callback(); + if (typeof global.__drainMicrotaskQueue === "function") { + global.__drainMicrotaskQueue(); + } + if (!shouldRepeat) { + pair.disposed = true; + directTimeoutCallbacks.delete(id); + } + }); + NSRunLoop.currentRunLoop.addTimerForMode(timer, NSRunLoopCommonModes); + pair.k = timer; + directTimeoutCallbacks.set(id, pair); + return id; + } + + global.setTimeout = function (callback, milliseconds) { + if (milliseconds === void 0) { milliseconds = 0; } + var args = []; + for (var i = 2; i < arguments.length; i++) { + args[i - 2] = arguments[i]; + } + return createDirectTimerAndGetId(function () { + return callback.apply(void 0, args); + }, milliseconds, false); + }; + + global.clearTimeout = function (id) { + var pair = directTimeoutCallbacks.get(id); + if (pair && !pair.disposed) { + pair.disposed = true; + pair.k.invalidate(); + directTimeoutCallbacks.delete(id); + } + }; + + global.setInterval = function (callback, milliseconds) { + if (milliseconds === void 0) { milliseconds = 0; } + var args = []; + for (var i = 2; i < arguments.length; i++) { + args[i - 2] = arguments[i]; + } + return createDirectTimerAndGetId(function () { + return callback.apply(void 0, args); + }, milliseconds, true); + }; + + global.clearInterval = global.clearTimeout; +} else { var timeoutCallbacks = new Map(); var timerId = 0; @@ -66,3 +141,4 @@ function clearTimeout(id) { global.setTimeout = setTimeout; global.clearTimeout = clearTimeout; +} diff --git a/test/runtime/runner/app/tests/Marshalling/FunctionPointerTests.js b/test/runtime/runner/app/tests/Marshalling/FunctionPointerTests.js index 392f72e8..691a2cf4 100644 --- a/test/runtime/runner/app/tests/Marshalling/FunctionPointerTests.js +++ b/test/runtime/runner/app/tests/Marshalling/FunctionPointerTests.js @@ -20,6 +20,21 @@ describe(module.id, function () { expect(TNSGetOutput()).toBe('4'); }); + it("SimpleFunctionPointerCallbackThread", function () { + if (!global.process || + !global.process.versions || + global.process.versions.engine !== "hermes") { + pending("Same-thread callback dispatch currently requires the Hermes thread-safe runtime."); + } + + var result = functionWithSimpleFunctionPointerOnBackground(function (nativeCallerWasMainThread) { + expect(nativeCallerWasMainThread).toBe(0); + return NSThread.isMainThread ? 1 : 0; + }); + + expect(result).toBe(0); + }); + it("SimpleFunctionPointerParameter", function () { var func = functionReturningFunctionPtrAsVoidPtr(); functionWithSimpleFunctionPointer(func); diff --git a/test/runtime/runner/app/tests/NativeApiJsiTests.js b/test/runtime/runner/app/tests/NativeApiJsiTests.js new file mode 100644 index 00000000..1f17f704 --- /dev/null +++ b/test/runtime/runner/app/tests/NativeApiJsiTests.js @@ -0,0 +1,40 @@ +describe("Native API JSI bridge", function () { + function apiOrPending() { + var api = global.__nativeScriptNativeApi; + if (!api) { + pending("Native API JSI bridge is only installed for Hermes."); + } + return api; + } + + afterEach(function () { + TNSClearOutput(); + }); + + it("exposes the Hermes JSI host object", function () { + var api = apiOrPending(); + + expect(api.runtime).toBe("jsi"); + expect(api.backend).toBe("hermes"); + expect(api.metadata.classes).toBeGreaterThan(0); + expect(api.metadata.functions).toBeGreaterThan(0); + expect(api.getClass("NSObject").available).toBe(true); + }); + + it("calls metadata-backed C functions through pure JSI", function () { + var api = apiOrPending(); + var fn = api.getFunction("functionWithInt"); + + expect(typeof fn).toBe("function"); + expect(fn(42)).toBe(42); + expect(TNSGetOutput()).toBe("42"); + }); + + it("sends Objective-C selectors through pure JSI", function () { + var api = apiOrPending(); + var primitives = api.getClass("TNSPrimitives").alloc().invoke("init"); + + expect(primitives.methodWithInt(24)).toBe(24); + expect(TNSGetOutput()).toBe("24"); + }); +}); diff --git a/test/runtime/runner/app/tests/Timers.js b/test/runtime/runner/app/tests/Timers.js index 7ee3c9fc..3159bad1 100644 --- a/test/runtime/runner/app/tests/Timers.js +++ b/test/runtime/runner/app/tests/Timers.js @@ -166,6 +166,11 @@ describe("native timer", () => { const baselineActiveTimerCount = getActiveTimerCount ? getActiveTimerCount() : null; + const baselineHermesTimerCallbackCount = + isHermes && + typeof global.__nsHermesTimerCallbackCount === "function" + ? global.__nsHermesTimerCallbackCount() + : null; { let obj = { value: 0, @@ -191,7 +196,7 @@ describe("native timer", () => { typeof global.__nsHermesHasTimerCallback === "function" ) { gc(); - expect(global.__nsHermesTimerCallbackCount()).toBe(0); + expect(global.__nsHermesTimerCallbackCount()).toBe(baselineHermesTimerCallbackCount); expect(global.__nsHermesHasTimerCallback(timeout.__timerId)).toBe(false); expect(global.__nsHermesHasTimerCallback(interval.__timerId)).toBe(false); done(); @@ -220,8 +225,14 @@ describe("native timer", () => { return; } + const defaultQosClass = + typeof qos_class_t !== "undefined" && + qos_class_t && + qos_class_t.QOS_CLASS_DEFAULT != null + ? qos_class_t.QOS_CLASS_DEFAULT + : (typeof QOS_CLASS_DEFAULT !== "undefined" ? QOS_CLASS_DEFAULT : 0); const background_queue = dispatch_get_global_queue( - qos_class_t.QOS_CLASS_DEFAULT, + Number(defaultQosClass), 0 ); const current_queue = dispatch_get_current_queue(); diff --git a/test/runtime/runner/app/tests/index.js b/test/runtime/runner/app/tests/index.js index be0e4671..a6949552 100644 --- a/test/runtime/runner/app/tests/index.js +++ b/test/runtime/runner/app/tests/index.js @@ -241,6 +241,7 @@ loadTest("./Promises"); loadTest("./Modules"); // loadTest("./RuntimeImplementedAPIs"); +loadTest("./NativeApiJsiTests"); loadTest("./WebNodeBuiltins"); loadTest("./VMTests"); @@ -266,6 +267,6 @@ execute(); if (typeof UIApplicationMain === "function") { UIApplicationMain(0, null, null, null); -} else if (typeof NSApplicationMain === "function") { +} else if (typeof NSApplicationMain === "function" && !logjunit) { NSApplicationMain(0, null); }