diff --git a/.github/actions/prepare_env/action.yml b/.github/actions/prepare_env/action.yml index ebcc8aed2..90854db0b 100644 --- a/.github/actions/prepare_env/action.yml +++ b/.github/actions/prepare_env/action.yml @@ -4,36 +4,35 @@ description: 'Prepare the CI environment by installing Swift and selected JDK et runs: using: composite steps: - - name: Install System Dependencies - run: apt-get -qq update && apt-get -qq install -y make curl wget libjemalloc2 libjemalloc-dev + - name: Check Swift version shell: bash - - name: Cache JDK - id: cache-jdk - uses: actions/cache@v4 - continue-on-error: true + run: swift -version + - name: Set up JDK ${{ matrix.jdk_version }} + uses: actions/setup-java@v4 with: - path: /usr/lib/jvm/default-jdk/ - key: ${{ runner.os }}-jdk-${{ matrix.jdk_vendor }}-${{ hashFiles('/usr/lib/jvm/default-jdk/*') }} - restore-keys: | - ${{ runner.os }}-jdk- - - name: Install JDK - if: steps.cache-jdk.outputs.cache-hit != 'true' - run: "bash -xc 'JDK_VENDOR=${{ matrix.jdk_vendor }} ./docker/install_jdk.sh'" + distribution: ${{ matrix.jdk_vendor }} + java-version: | + 25 + 17 + cache: 'gradle' + - name: Set JAVA_HOME_{N} shell: bash - # TODO: not using setup-java since incompatible with the swiftlang/swift base image - # - name: Install Untested Nightly Swift - # run: "bash -xc './docker/install_untested_nightly_swift.sh'" - - name: Cache local Gradle repository - uses: actions/cache@v4 - continue-on-error: true - with: - path: | - /root/.gradle/caches - /root/.gradle/wrapper - key: ${{ runner.os }}-gradle-${{ hashFiles('*/*.gradle*', 'settings.gradle') }} - restore-keys: | - ${{ runner.os }}-gradle- + run: | + if [[ -n "$JAVA_HOME_21_X64" ]]; then + echo "JAVA_HOME_21=$JAVA_HOME_21_X64" >> $GITHUB_ENV + elif [[ -n "$JAVA_HOME_21_ARM64" ]]; then + echo "JAVA_HOME_21=$JAVA_HOME_21_ARM64" >> $GITHUB_ENV + fi + if [[ -n "$JAVA_HOME_25_X64" ]]; then + echo "JAVA_HOME_25=$JAVA_HOME_25_X64" >> $GITHUB_ENV + elif [[ -n "$JAVA_HOME_25_ARM64" ]]; then + echo "JAVA_HOME_25=$JAVA_HOME_25_ARM64" >> $GITHUB_ENV + fi + # - name: Check Java environment + # shell: bash + # run: ./gradlew -q javaToolchains - name: Cache local SwiftPM repository + if: matrix.os_version == 'jammy' uses: actions/cache@v4 continue-on-error: true with: diff --git a/.github/scripts/install_swiftly.sh b/.github/scripts/install_swiftly.sh new file mode 100755 index 000000000..e0792894f --- /dev/null +++ b/.github/scripts/install_swiftly.sh @@ -0,0 +1,92 @@ +#!/bin/bash + +# This script is reused from Swiftly itself, see: +# https://github.com/swiftlang/swiftly/blob/main/scripts/prep-gh-action.sh +# +# This script does a bit of extra preparation of the docker containers used to run the GitHub workflows +# that are specific to this project's needs when building/testing. Note that this script runs on +# every supported Linux distribution and macOS so it must adapt to the distribution that it is running. + +if [[ "$(uname -s)" == "Linux" ]]; then + # Install the basic utilities depending on the type of Linux distribution + apt-get --help && apt-get update && TZ=Etc/UTC apt-get -y install curl make gpg tzdata + yum --help && (curl --help && yum -y install curl) && yum install make gpg +fi + +set -e + +while [ $# -ne 0 ]; do + arg="$1" + case "$arg" in + snapshot) + swiftMainSnapshot=true + ;; + *) + ;; + esac + shift +done + +echo "Installing swiftly" + +if [[ "$(uname -s)" == "Linux" ]]; then + curl -O "https://download.swift.org/swiftly/linux/swiftly-$(uname -m).tar.gz" && tar zxf swiftly-*.tar.gz && ./swiftly init -y --skip-install + # shellcheck disable=SC1091 + . "/root/.local/share/swiftly/env.sh" +else + # shellcheck disable=SC2155 + export SWIFTLY_HOME_DIR="$(pwd)/swiftly-bootstrap" + export SWIFTLY_BIN_DIR="$SWIFTLY_HOME_DIR/bin" + export SWIFTLY_TOOLCHAINS_DIR="$SWIFTLY_HOME_DIR/toolchains" + + curl -O https://download.swift.org/swiftly/darwin/swiftly.pkg && pkgutil --check-signature swiftly.pkg && pkgutil --verbose --expand swiftly.pkg "${SWIFTLY_HOME_DIR}" && tar -C "${SWIFTLY_HOME_DIR}" -xvf "${SWIFTLY_HOME_DIR}"/swiftly-*/Payload && "$SWIFTLY_HOME_DIR/bin/swiftly" init -y --skip-install + + chmod +x "$SWIFTLY_HOME_DIR/env.sh" + # shellcheck disable=SC1091 + . "$SWIFTLY_HOME_DIR/env.sh" +fi + +hash -r + +if [ -n "$GITHUB_ENV" ]; then + echo "Updating GitHub environment" + echo "PATH=$PATH" >> "$GITHUB_ENV" && echo "SWIFTLY_HOME_DIR=$SWIFTLY_HOME_DIR" >> "$GITHUB_ENV" && echo "SWIFTLY_BIN_DIR=$SWIFTLY_BIN_DIR" >> "$GITHUB_ENV" && echo "SWIFTLY_TOOLCHAINS_DIR=$SWIFTLY_TOOLCHAINS_DIR" >> "$GITHUB_ENV" +fi + +selector=() +runSelector=() + +if [ "$swiftMainSnapshot" == true ]; then + echo "Installing latest main-snapshot toolchain" + selector=("main-snapshot") + runSelector=("+main-snapshot") +elif [ -n "${SWIFT_VERSION}" ]; then + echo "Installing selected swift toolchain from SWIFT_VERSION environment variable" + selector=("${SWIFT_VERSION}") + runSelector=() +elif [ -f .swift-version ]; then + echo "Installing selected swift toolchain from .swift-version file" + selector=() + runSelector=() +else + echo "Installing latest toolchain" + selector=("latest") + runSelector=("+latest") +fi + +swiftly install --post-install-file=post-install.sh "${selector[@]}" + +if [ -f post-install.sh ]; then + echo "Performing swift toolchain post-installation" + chmod u+x post-install.sh && ./post-install.sh +fi + +echo "Displaying swift version" +swiftly run "${runSelector[@]}" swift --version + +if [[ "$(uname -s)" == "Linux" ]]; then + if [[ -f "$(dirname "$0")/install-libarchive.sh" ]]; then + CC=clang swiftly run "${runSelector[@]}" "$(dirname "$0")/install-libarchive.sh" + fi +fi + diff --git a/.github/scripts/validate_docs.sh b/.github/scripts/validate_docs.sh new file mode 100755 index 000000000..5e5539430 --- /dev/null +++ b/.github/scripts/validate_docs.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +set -e +set -x + +DEPENDENCY='.package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.0.0")' + +if grep -q "$DEPENDENCY" Package.swift; then + echo "Package.swift already contains 'swift-docc-plugin" +else + cat <> Package.swift + +package.dependencies.append( + $DEPENDENCY +) +EOF +fi + +swift package --disable-sandbox plugin generate-documentation --target "SwiftJavaDocumentation" --warnings-as-errors --analyze diff --git a/.github/scripts/validate_sample.sh b/.github/scripts/validate_sample.sh index 7e0ab3d2c..64265b01f 100755 --- a/.github/scripts/validate_sample.sh +++ b/.github/scripts/validate_sample.sh @@ -1,5 +1,8 @@ #!/bin/bash +set -e +set -x + # shellcheck disable=SC2034 declare -r GREEN='\033[0;32m' declare -r BOLD='\033[1m' @@ -8,20 +11,19 @@ declare -r RESET='\033[0m' declare -r sampleDir="$1" declare -r CI_VALIDATE_SCRIPT='ci-validate.sh' +echo "Using Swift: $(which swift)" + echo "" echo "" echo "========================================================================" printf "Validate sample '${BOLD}%s${RESET}' using: " "$sampleDir" cd "$sampleDir" || exit if [[ $(find . -name ${CI_VALIDATE_SCRIPT} -maxdepth 1) ]]; then - echo -e "Custom ${BOLD}${CI_VALIDATE_SCRIPT}${RESET} script..." - ./${CI_VALIDATE_SCRIPT} || exit -elif [[ $(find . -name 'build.gradle*' -maxdepth 1) ]]; then - echo -e "${BOLD}Gradle${RESET} build..." - ./gradlew build || ./gradlew build --info # re-run to get better failure output + echo -e "Run ${BOLD}${CI_VALIDATE_SCRIPT}${RESET} script..." + ./${CI_VALIDATE_SCRIPT} else - echo -e "${BOLD}SwiftPM${RESET} build..." - swift build || exit + echo -e "${BOLD}Missing ${CI_VALIDATE_SCRIPT} file!${RESET}" + exit fi echo -e "Validated sample '${BOLD}${sampleDir}${RESET}': ${BOLD}passed${RESET}." diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 8fa774bd0..a0f2eda5f 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -1,5 +1,8 @@ name: pull_request +permissions: + contents: read + on: pull_request: types: [opened, reopened, synchronize] @@ -13,177 +16,243 @@ jobs: # FIXME: Something is off with the format task and it gets "stuck", need to investigate format_check_enabled: false license_header_check_project_name: Swift.org + # Since we need JAVA_HOME to be set up for building the project and depending on a different workflow won't + # give us that, we disable the checking and make a separate job that performs docs validation + docs_check_enabled: false + + # This replicates 'docs-check' from https://github.com/swiftlang/github-workflows/blob/main/.github/workflows/soundness.yml + # because we need to set up environment so we can build the SwiftJava project (including Java runtime/dependencies). + soundness-docs: + name: Documentation check + runs-on: ubuntu-latest + container: + image: 'swift:6.2-noble' + strategy: + fail-fast: true + matrix: + swift_version: ['6.2'] + os_version: ['jammy'] + jdk_vendor: ['corretto'] + steps: + - uses: actions/checkout@v4 + - name: Prepare CI Environment + uses: ./.github/actions/prepare_env + - name: Swift Build + run: swift build --disable-experimental-prebuilts # FIXME: until prebuilt swift-syntax isn't broken on 6.2 anymore: https://github.com/swiftlang/swift-java/issues/418 + - name: Run documentation check + run: ./.github/scripts/validate_docs.sh test-java: - name: Java tests (swift:${{ matrix.swift_version }} jdk:${{matrix.jdk_vendor}} os:${{ matrix.os_version }}) + name: Test (Java) (${{ matrix.os_version }} swift:${{ matrix.swift_version }} jdk:${{matrix.jdk_vendor}}) runs-on: ubuntu-latest strategy: fail-fast: true matrix: - # swift_version: ['nightly-main'] - swift_version: ['6.0.2'] + swift_version: ['6.2', 'nightly'] os_version: ['jammy'] - jdk_vendor: ['Corretto'] + jdk_vendor: ['corretto'] container: image: ${{ (contains(matrix.swift_version, 'nightly') && 'swiftlang/swift') || 'swift' }}:${{ matrix.swift_version }}-${{ matrix.os_version }} env: - JAVA_HOME: "/usr/lib/jvm/default-jdk" SWIFT_JAVA_VERBOSE: true steps: - uses: actions/checkout@v4 - name: Prepare CI Environment uses: ./.github/actions/prepare_env - - name: Gradle :SwiftKit:build - run: ./gradlew build -x test - - name: Gradle :SwiftKit:check - run: ./gradlew :SwiftKit:check --info - - name: Gradle compile JMH benchmarks - run: ./gradlew compileJmh --info + - name: Gradle :SwiftKitCore:build + run: ./gradlew :SwiftKitCore:build -x test + - name: Gradle :SwiftKitCore:check + run: ./gradlew :SwiftKitCore:check --info + - name: Gradle :SwiftKitFFM:build + run: ./gradlew :SwiftKitFFM:build -x test + - name: Gradle :SwiftKitFFM:check + run: ./gradlew :SwiftKitFFM:check --info - test-swift: - name: Swift tests (swift:${{ matrix.swift_version }} jdk:${{matrix.jdk_vendor}} os:${{ matrix.os_version }}) - runs-on: ubuntu-latest + test-java-macos: + name: Test (Java) (${{ matrix.os_version }} swift:${{ matrix.swift_version }} jdk:${{matrix.jdk_vendor}}) + runs-on: [self-hosted, macos, sequoia, ARM64] strategy: - fail-fast: false + fail-fast: true matrix: - # swift_version: ['nightly-main'] - swift_version: ['6.0.2'] - os_version: ['jammy'] - jdk_vendor: ['Corretto'] - container: - image: ${{ (contains(matrix.swift_version, 'nightly') && 'swiftlang/swift') || 'swift' }}:${{ matrix.swift_version }}-${{ matrix.os_version }} + swift_version: ['6.2'] + os_version: ['macos'] + jdk_vendor: ['corretto'] env: - JAVA_HOME: "/usr/lib/jvm/default-jdk" SWIFT_JAVA_VERBOSE: true steps: - uses: actions/checkout@v4 - name: Prepare CI Environment uses: ./.github/actions/prepare_env - - name: Swift Build - run: "swift build --build-tests --disable-sandbox" - - name: Swift Test - run: "swift test" + - name: Gradle :SwiftKitCore:build + run: ./gradlew :SwiftKitCore:build -x test + - name: Gradle :SwiftKitCore:check + run: ./gradlew :SwiftKitCore:check --debug + - name: Gradle :SwiftKitFFM:build + run: ./gradlew :SwiftKitFFM:build -x test + - name: Gradle :SwiftKitFFM:check + run: ./gradlew :SwiftKitFFM:check --debug - verify-sample-01: - name: Verify Sample JavaDependencySampleApp (swift:${{ matrix.swift_version }} jdk:${{matrix.jdk_vendor}} os:${{ matrix.os_version }}) + benchmark-java: + name: Benchmark (JMH) (${{ matrix.os_version }} swift:${{ matrix.swift_version }} jdk:${{matrix.jdk_vendor}}) runs-on: ubuntu-latest strategy: - fail-fast: false + fail-fast: true matrix: - # swift_version: ['nightly-main'] - swift_version: ['6.0.2'] + swift_version: ['6.2'] os_version: ['jammy'] - jdk_vendor: ['Corretto'] + jdk_vendor: ['corretto'] container: image: ${{ (contains(matrix.swift_version, 'nightly') && 'swiftlang/swift') || 'swift' }}:${{ matrix.swift_version }}-${{ matrix.os_version }} env: - JAVA_HOME: "/usr/lib/jvm/default-jdk" + SWIFT_JAVA_VERBOSE: true steps: - uses: actions/checkout@v4 - name: Prepare CI Environment uses: ./.github/actions/prepare_env - - name: "Verify Sample: JavaDependencySampleApp" - run: .github/scripts/validate_sample.sh Samples/JavaDependencySampleApp - verify-sample-02: - name: Verify Sample JavaKitSampleApp (swift:${{ matrix.swift_version }} jdk:${{matrix.jdk_vendor}} os:${{ matrix.os_version }}) + - name: Gradle compile JMH benchmarks + run: ./gradlew compileJmh --info + + benchmark-swift: + name: Benchmark (Swift) (${{ matrix.os_version }} swift:${{ matrix.swift_version }} jdk:${{matrix.jdk_vendor}}) runs-on: ubuntu-latest strategy: - fail-fast: false + fail-fast: true matrix: - # swift_version: ['nightly-main'] - swift_version: ['6.0.2'] + swift_version: ['6.2'] os_version: ['jammy'] - jdk_vendor: ['Corretto'] + jdk_vendor: ['corretto'] container: image: ${{ (contains(matrix.swift_version, 'nightly') && 'swiftlang/swift') || 'swift' }}:${{ matrix.swift_version }}-${{ matrix.os_version }} env: - JAVA_HOME: "/usr/lib/jvm/default-jdk" + SWIFT_JAVA_VERBOSE: true steps: - uses: actions/checkout@v4 - name: Prepare CI Environment uses: ./.github/actions/prepare_env - - name: "Verify Sample" - run: .github/scripts/validate_sample.sh Samples/JavaKitSampleApp - verify-sample-03: - name: Verify Sample JavaProbablyPrime (swift:${{ matrix.swift_version }} jdk:${{matrix.jdk_vendor}} os:${{ matrix.os_version }}) + - name: Install jemalloc + run: apt-get update && apt-get install -y libjemalloc-dev + - name: Swift Benchmarks + run: swift package --package-path Benchmarks/ --disable-experimental-prebuilts benchmark # FIXME: until prebuilt swift-syntax isn't broken on 6.2 anymore: https://github.com/swiftlang/swift-java/issues/418 + + test-swift: + name: Test (Swift) (${{ matrix.os_version }} swift:${{ matrix.swift_version }} jdk:${{matrix.jdk_vendor}}) runs-on: ubuntu-latest strategy: fail-fast: false matrix: - # swift_version: ['nightly-main'] - swift_version: ['6.0.2'] + swift_version: ['6.1.3', '6.2', 'nightly'] os_version: ['jammy'] - jdk_vendor: ['Corretto'] + jdk_vendor: ['corretto'] container: image: ${{ (contains(matrix.swift_version, 'nightly') && 'swiftlang/swift') || 'swift' }}:${{ matrix.swift_version }}-${{ matrix.os_version }} env: - JAVA_HOME: "/usr/lib/jvm/default-jdk" + SWIFT_JAVA_VERBOSE: true steps: - uses: actions/checkout@v4 - name: Prepare CI Environment uses: ./.github/actions/prepare_env - - name: "Verify Sample" - run: .github/scripts/validate_sample.sh Samples/JavaProbablyPrime - verify-sample-04: - name: Verify Sample JavaSieve (swift:${{ matrix.swift_version }} jdk:${{matrix.jdk_vendor}} os:${{ matrix.os_version }}) - runs-on: ubuntu-latest + - name: Swift Build + run: swift build --build-tests --disable-sandbox --disable-experimental-prebuilts # FIXME: until prebuilt swift-syntax isn't broken on 6.2 anymore: https://github.com/swiftlang/swift-java/issues/418 + - name: Swift Test + run: swift test --disable-experimental-prebuilts # FIXME: until prebuilt swift-syntax isn't broken on 6.2 anymore: https://github.com/swiftlang/swift-java/issues/418 + + test-swift-macos: + name: Test (Swift) (${{ matrix.os_version }} swift:${{ matrix.swift_version }} jdk:${{matrix.jdk_vendor}}) + runs-on: [self-hosted, macos, sequoia, ARM64] strategy: fail-fast: false matrix: - # swift_version: ['nightly-main'] - swift_version: ['6.0.2'] - os_version: ['jammy'] - jdk_vendor: ['Corretto'] - container: - image: ${{ (contains(matrix.swift_version, 'nightly') && 'swiftlang/swift') || 'swift' }}:${{ matrix.swift_version }}-${{ matrix.os_version }} + swift_version: ['6.2'] + os_version: ['macos'] + jdk_vendor: ['corretto'] env: - JAVA_HOME: "/usr/lib/jvm/default-jdk" + SWIFT_JAVA_VERBOSE: true steps: - uses: actions/checkout@v4 - name: Prepare CI Environment uses: ./.github/actions/prepare_env - - name: "Verify Sample" - run: .github/scripts/validate_sample.sh Samples/JavaSieve - verify-sample-05: - name: Verify Sample SwiftAndJavaJarSampleLib (swift:${{ matrix.swift_version }} jdk:${{matrix.jdk_vendor}} os:${{ matrix.os_version }}) + - name: Swift Build + run: "swift build --build-tests --disable-sandbox" + - name: Swift Test + run: "swift test" + + build-swift-android: + name: Sample SwiftJavaExtractJNISampleApp (Android) (${{ matrix.os_version }} swift:${{ matrix.swift_version }} jdk:${{matrix.jdk_vendor}} android:${{matrix.sdk_triple}}) runs-on: ubuntu-latest strategy: fail-fast: false matrix: - # swift_version: ['nightly-main'] - swift_version: ['6.0.2'] + swift_version: ['nightly-main'] os_version: ['jammy'] - jdk_vendor: ['Corretto'] + jdk_vendor: ['corretto'] + sdk_triple: ['x86_64-unknown-linux-android28'] + ndk_version: ['r27d'] container: image: ${{ (contains(matrix.swift_version, 'nightly') && 'swiftlang/swift') || 'swift' }}:${{ matrix.swift_version }}-${{ matrix.os_version }} - env: - JAVA_HOME: "/usr/lib/jvm/default-jdk" steps: - uses: actions/checkout@v4 - name: Prepare CI Environment uses: ./.github/actions/prepare_env - - name: "Verify Sample" - run: .github/scripts/validate_sample.sh Samples/SwiftAndJavaJarSampleLib - verify-sample-06: - name: Verify Sample SwiftKitSampleApp (swift:${{ matrix.swift_version }} jdk:${{matrix.jdk_vendor}} os:${{ matrix.os_version }}) + - name: Install Swift SDK for Android and build + run: | + apt-get -q update && apt-get -yq install curl + cd Samples/SwiftJavaExtractJNISampleApp + curl -s --retry 3 https://raw.githubusercontent.com/swiftlang/github-workflows/refs/heads/main/.github/workflows/scripts/install-and-build-with-sdk.sh | \ + bash -s -- --android --build-command="swift build" --android-sdk-triple="${{ matrix.sdk_triple }}" --android-ndk-version="${{ matrix.ndk_version }}" ${{ matrix.swift_version }} + + verify-samples: + name: Sample ${{ matrix.sample_app }} (${{ matrix.os_version }} swift:${{ matrix.swift_version }} jdk:${{matrix.jdk_vendor}}) runs-on: ubuntu-latest strategy: fail-fast: false matrix: - # swift_version: ['nightly-main'] - swift_version: ['6.0.2'] + swift_version: ['6.1.3', '6.2', 'nightly'] os_version: ['jammy'] - jdk_vendor: ['Corretto'] + jdk_vendor: ['corretto'] + sample_app: [ # TODO: use a reusable-workflow to generate those names + 'JavaDependencySampleApp', + 'JavaKitSampleApp', + 'JavaProbablyPrime', + 'JavaSieve', + 'SwiftAndJavaJarSampleLib', + 'SwiftJavaExtractFFMSampleApp', + 'SwiftJavaExtractJNISampleApp', + ] container: image: ${{ (contains(matrix.swift_version, 'nightly') && 'swiftlang/swift') || 'swift' }}:${{ matrix.swift_version }}-${{ matrix.os_version }} - env: - JAVA_HOME: "/usr/lib/jvm/default-jdk" steps: - uses: actions/checkout@v4 - name: Prepare CI Environment uses: ./.github/actions/prepare_env - - name: "Verify Sample" - run: .github/scripts/validate_sample.sh Samples/SwiftKitSampleApp - # TODO: Benchmark compile crashes in CI, enable when nightly toolchains in better shape. - # - name: Build (Swift) Benchmarks - # run: "swift package --package-path Benchmarks/ benchmark list" + - name: "Verify sample: ${{ matrix.sample_app }}" + run: .github/scripts/validate_sample.sh Samples/${{ matrix.sample_app }} + + + verify-samples-macos: + name: Sample ${{ matrix.sample_app }} (${{ matrix.os_version }} swift:${{ matrix.swift_version }} jdk:${{matrix.jdk_vendor}}) + runs-on: [self-hosted, macos, sequoia, ARM64] + strategy: + fail-fast: false + matrix: + swift_version: ['6.2'] # no nightly testing on macOS + os_version: ['macos'] + jdk_vendor: ['corretto'] + sample_app: [ # TODO: use a reusable-workflow to generate those names + 'JavaDependencySampleApp', + 'JavaKitSampleApp', + 'JavaProbablyPrime', + 'JavaSieve', + 'SwiftAndJavaJarSampleLib', + 'SwiftJavaExtractFFMSampleApp', + 'SwiftJavaExtractJNISampleApp', + ] + steps: + - uses: actions/checkout@v4 + - name: Prepare CI Environment + uses: ./.github/actions/prepare_env + - name: Install Swiftly # we specifically install Swiftly in macOS jobs because we want a simpler way to find the right dylib paths for libraries + run: ./.github/scripts/install_swiftly.sh + env: + SWIFT_VERSION: "${{ matrix.swift_version }}" + - name: "Verify sample ${{ matrix.sample_app }}" + run: .github/scripts/validate_sample.sh Samples/${{ matrix.sample_app }} diff --git a/.gitignore b/.gitignore index 6712182f3..d4e1c5ec4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ +.swift-version +.sdkmanrc + .DS_Store .build .idea +.vscode Packages xcuserdata/ DerivedData/ @@ -13,6 +17,7 @@ bin/ BuildLogic/out/ .index-build .build-vscode +**/.vscode # Ignore gradle build artifacts .gradle @@ -41,3 +46,5 @@ Package.resolved */**/*.o */**/*.swiftdeps */**/*.swiftdeps~ +*/**/.docc-build/ + diff --git a/.licenseignore b/.licenseignore index df3de377a..a49ab625d 100644 --- a/.licenseignore +++ b/.licenseignore @@ -35,6 +35,8 @@ Makefile **/CMakeLists.txt **/*.jar **/generated/*.java +gradle.properties +**/gradle.properties **/generated/*.swift gradle/wrapper/gradle-wrapper.properties gradlew @@ -47,5 +49,6 @@ Plugins/**/_PluginsShared Plugins/**/0_PLEASE_SYMLINK* Plugins/PluginsShared/JavaKitConfigurationShared Samples/JavaDependencySampleApp/gradle -Sources/_Subprocess/_nio_locks.swift +Sources/_Subprocess/** +Sources/_SubprocessCShims/** Samples/gradle diff --git a/.spi.yml b/.spi.yml new file mode 100644 index 000000000..f4074e0e3 --- /dev/null +++ b/.spi.yml @@ -0,0 +1,8 @@ +version: 1 +builder: + configs: + - documentation_targets: [ + SwiftJavaDocumentation + ] + # Drop this version pinning once 6.2 is released and docs are built with 6.2 by default to prevent it staying on 6.2 forever. + swift_version: 6.2 diff --git a/.unacceptablelanguageignore b/.unacceptablelanguageignore index 0310d1c36..c3f97d3d4 100644 --- a/.unacceptablelanguageignore +++ b/.unacceptablelanguageignore @@ -2,16 +2,6 @@ Sources/SwiftJavaBootstrapJavaTool/SwiftJavaBootstrapJavaTool.swift Sources/_Subprocess/Platforms/Subprocess+Darwin.swift Sources/_Subprocess/Platforms/Subprocess+Linux.swift Sources/_Subprocess/Platforms/Subprocess+Unix.swift -Sources/_Subprocess/Platforms/Subprocess+Unix.swift -Sources/_Subprocess/Platforms/Subprocess+Unix.swift -Sources/_Subprocess/Platforms/Subprocess+Unix.swift -Sources/_Subprocess/Platforms/Subprocess+Unix.swift -Sources/_Subprocess/Platforms/Subprocess+Unix.swift -Sources/_Subprocess/Subprocess+Teardown.swift -Sources/_Subprocess/Subprocess+Teardown.swift -Sources/_Subprocess/Subprocess+Teardown.swift -Sources/_Subprocess/Subprocess+Teardown.swift -Sources/_Subprocess/Subprocess+Teardown.swift -Sources/_Subprocess/Subprocess+Teardown.swift -Sources/_Subprocess/Subprocess+Teardown.swift -Sources/_Subprocess/Subprocess.swift \ No newline at end of file +Sources/_Subprocess/Teardown.swift +Sources/_Subprocess/Subprocess.swift +NOTICE.txt \ No newline at end of file diff --git a/Benchmarks/Benchmarks/JavaApiCallBenchmarks/JavaApiCallBenchmarks.swift b/Benchmarks/Benchmarks/JavaApiCallBenchmarks/JavaApiCallBenchmarks.swift index 25c670747..d52d31330 100644 --- a/Benchmarks/Benchmarks/JavaApiCallBenchmarks/JavaApiCallBenchmarks.swift +++ b/Benchmarks/Benchmarks/JavaApiCallBenchmarks/JavaApiCallBenchmarks.swift @@ -14,8 +14,8 @@ import Benchmark import Foundation -import JavaKit -import JavaKitNetwork +import SwiftJava +import JavaNet @MainActor let benchmarks = { var jvm: JavaVirtualMachine { diff --git a/Benchmarks/Package.swift b/Benchmarks/Package.swift index b8fbe580e..127e4d483 100644 --- a/Benchmarks/Package.swift +++ b/Benchmarks/Package.swift @@ -51,9 +51,9 @@ let package = Package( .executableTarget( name: "JavaApiCallBenchmarks", dependencies: [ - .product(name: "JavaRuntime", package: "swift-java"), - .product(name: "JavaKit", package: "swift-java"), - .product(name: "JavaKitNetwork", package: "swift-java"), + .product(name: "CSwiftJavaJNI", package: "swift-java"), + .product(name: "SwiftJava", package: "swift-java"), + .product(name: "JavaNet", package: "swift-java"), .product(name: "Benchmark", package: "package-benchmark"), ], path: "Benchmarks/JavaApiCallBenchmarks", diff --git a/BuildLogic/src/main/kotlin/build-logic.java-common-conventions.gradle.kts b/BuildLogic/src/main/kotlin/build-logic.java-common-conventions.gradle.kts index ef88ce157..1f2df6e59 100644 --- a/BuildLogic/src/main/kotlin/build-logic.java-common-conventions.gradle.kts +++ b/BuildLogic/src/main/kotlin/build-logic.java-common-conventions.gradle.kts @@ -23,7 +23,7 @@ plugins { java { toolchain { - languageVersion = JavaLanguageVersion.of(22) + languageVersion = JavaLanguageVersion.of(25) } } @@ -31,21 +31,6 @@ repositories { mavenCentral() } -testing { - suites { - val test by getting(JvmTestSuite::class) { - useJUnitJupiter("5.10.3") - } - } -} - -/// Enable access to preview APIs, e.g. java.lang.foreign.* (Panama) -tasks.withType(JavaCompile::class).forEach { - it.options.compilerArgs.add("--enable-preview") - it.options.compilerArgs.add("-Xlint:preview") -} - - fun getSwiftRuntimeLibraryPaths(): List { val process = ProcessBuilder("swiftc", "-print-target-info") .redirectError(ProcessBuilder.Redirect.INHERIT) diff --git a/Package.swift b/Package.swift index 47312ccd7..2c56a2870 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 6.0 +// swift-tools-version: 6.1 // The swift-tools-version declares the minimum version of Swift required to build this package. import CompilerPluginSupport @@ -11,8 +11,12 @@ import Foundation // Library/Java/JavaVirtualMachines/openjdk-21.jdk/Contents/Home. func findJavaHome() -> String { if let home = ProcessInfo.processInfo.environment["JAVA_HOME"] { + print("JAVA_HOME = \(home)") return home } + if let opts = ProcessInfo.processInfo.environment["SWIFT_JAVA_JAVA_OPTS"] { + print("SWIFT_JAVA_JAVA_OPTS = \(opts)") + } // This is a workaround for envs (some IDEs) which have trouble with // picking up env variables during the build process @@ -30,6 +34,11 @@ func findJavaHome() -> String { return home } + + if ProcessInfo.processInfo.environment["SPI_PROCESSING"] == "1" && ProcessInfo.processInfo.environment["SPI_BUILD"] == nil { + // Just ignore that we're missing a JAVA_HOME when building in Swift Package Index during general processing where no Java is needed. However, do _not_ suppress the error during SPI's compatibility build stage where Java is required. + return "" + } fatalError("Please set the JAVA_HOME environment variable to point to where Java is installed.") } @@ -83,50 +92,50 @@ let javaIncludePath = "\(javaHome)/include" #endif let package = Package( - name: "SwiftJava", + name: "swift-java", platforms: [ - .macOS(.v10_15) + .macOS(.v15) ], products: [ - // ==== JavaKit (i.e. calling Java directly Swift utilities) + // ==== SwiftJava (i.e. calling Java directly Swift utilities) .library( - name: "JavaKit", - targets: ["JavaKit"] + name: "SwiftJava", + targets: ["SwiftJava"] ), .library( - name: "JavaRuntime", - targets: ["JavaRuntime"] + name: "CSwiftJavaJNI", + targets: ["CSwiftJavaJNI"] ), .library( - name: "JavaKitCollection", - targets: ["JavaKitCollection"] + name: "JavaUtil", + targets: ["JavaUtil"] ), .library( - name: "JavaKitFunction", - targets: ["JavaKitFunction"] + name: "JavaUtilFunction", + targets: ["JavaUtilFunction"] ), .library( - name: "JavaKitJar", - targets: ["JavaKitJar"] + name: "JavaUtilJar", + targets: ["JavaUtilJar"] ), .library( - name: "JavaKitNetwork", - targets: ["JavaKitNetwork"] + name: "JavaNet", + targets: ["JavaNet"] ), .library( - name: "JavaKitIO", - targets: ["JavaKitIO"] + name: "JavaIO", + targets: ["JavaIO"] ), .library( - name: "JavaKitReflection", - targets: ["JavaKitReflection"] + name: "JavaLangReflect", + targets: ["JavaLangReflect"] ), .library( @@ -138,6 +147,12 @@ let package = Package( name: "swift-java", targets: ["SwiftJavaTool"] ), + + + .library( + name: "SwiftJavaDocumentation", + targets: ["SwiftJavaDocumentation"] + ), // ==== Plugin for building Java code .plugin( @@ -157,9 +172,14 @@ let package = Package( // Support library written in Swift for SwiftKit "Java" .library( - name: "SwiftKitSwift", + name: "SwiftJavaRuntimeSupport", + targets: ["SwiftJavaRuntimeSupport"] + ), + + .library( + name: "SwiftRuntimeFunctions", type: .dynamic, - targets: ["SwiftKitSwift"] + targets: ["SwiftRuntimeFunctions"] ), .library( @@ -174,12 +194,6 @@ let package = Package( "JExtractSwiftPlugin" ] ), - .plugin( - name: "JExtractSwiftCommandPlugin", - targets: [ - "JExtractSwiftCommandPlugin" - ] - ), // ==== Examples @@ -191,16 +205,31 @@ let package = Package( ], dependencies: [ - .package(url: "https://github.com/swiftlang/swift-syntax", from: "600.0.1"), + .package(url: "https://github.com/swiftlang/swift-syntax", from: "602.0.0"), .package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0"), .package(url: "https://github.com/apple/swift-system", from: "1.4.0"), + .package(url: "https://github.com/apple/swift-log", from: "1.2.0"), + .package(url: "https://github.com/apple/swift-collections", .upToNextMinor(from: "1.3.0")), // primarily for ordered collections + +// // FIXME: swift-subprocess stopped supporting 6.0 when it moved into a package; +// // we'll need to drop 6.0 as well, but currently blocked on doing so by swiftpm plugin pending design questions +// .package(url: "https://github.com/swiftlang/swift-subprocess.git", revision: "de15b67f7871c8a039ef7f4813eb39a8878f61a6"), // Benchmarking .package(url: "https://github.com/ordo-one/package-benchmark", .upToNextMajor(from: "1.4.0")), ], targets: [ + .target( + name: "SwiftJavaDocumentation", + dependencies: [ + "SwiftJava", + "SwiftJavaRuntimeSupport", + "SwiftRuntimeFunctions", + ] + ), + .macro( - name: "JavaKitMacros", + name: "SwiftJavaMacros", dependencies: [ .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), @@ -217,17 +246,18 @@ let package = Package( ), .target( - name: "JavaKit", + name: "SwiftJava", dependencies: [ - "JavaRuntime", - "JavaKitMacros", + "CSwiftJavaJNI", + "SwiftJavaMacros", "JavaTypes", - "JavaKitConfigurationShared", // for Configuration reading at runtime + "SwiftJavaConfigurationShared", // for Configuration reading at runtime ], exclude: ["swift-java.config"], swiftSettings: [ .swiftLanguageMode(.v5), - .unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"]) + .unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"], .when(platforms: [.macOS, .linux, .windows])), + .unsafeFlags(["-Xfrontend", "-sil-verify-none"], .when(configuration: .release)), // Workaround for https://github.com/swiftlang/swift/issues/84899 ], linkerSettings: [ .unsafeFlags( @@ -250,8 +280,9 @@ let package = Package( ] ), .target( - name: "JavaKitCollection", - dependencies: ["JavaKit"], + name: "JavaUtil", + dependencies: ["SwiftJava"], + path: "Sources/JavaStdlib/JavaUtil", exclude: ["swift-java.config"], swiftSettings: [ .swiftLanguageMode(.v5), @@ -259,8 +290,9 @@ let package = Package( ] ), .target( - name: "JavaKitFunction", - dependencies: ["JavaKit"], + name: "JavaUtilFunction", + dependencies: ["SwiftJava"], + path: "Sources/JavaStdlib/JavaUtilFunction", exclude: ["swift-java.config"], swiftSettings: [ .swiftLanguageMode(.v5), @@ -268,8 +300,9 @@ let package = Package( ] ), .target( - name: "JavaKitJar", - dependencies: ["JavaKit", "JavaKitCollection"], + name: "JavaUtilJar", + dependencies: ["SwiftJava", "JavaUtil"], + path: "Sources/JavaStdlib/JavaUtilJar", exclude: ["swift-java.config"], swiftSettings: [ .swiftLanguageMode(.v5), @@ -277,8 +310,9 @@ let package = Package( ] ), .target( - name: "JavaKitNetwork", - dependencies: ["JavaKit", "JavaKitCollection"], + name: "JavaNet", + dependencies: ["SwiftJava", "JavaUtil"], + path: "Sources/JavaStdlib/JavaNet", exclude: ["swift-java.config"], swiftSettings: [ .swiftLanguageMode(.v5), @@ -286,8 +320,9 @@ let package = Package( ] ), .target( - name: "JavaKitIO", - dependencies: ["JavaKit", "JavaKitCollection"], + name: "JavaIO", + dependencies: ["SwiftJava", "JavaUtil"], + path: "Sources/JavaStdlib/JavaIO", exclude: ["swift-java.config"], swiftSettings: [ .swiftLanguageMode(.v5), @@ -295,8 +330,9 @@ let package = Package( ] ), .target( - name: "JavaKitReflection", - dependencies: ["JavaKit", "JavaKitCollection"], + name: "JavaLangReflect", + dependencies: ["SwiftJava", "JavaUtil"], + path: "Sources/JavaStdlib/JavaLangReflect", exclude: ["swift-java.config"], swiftSettings: [ .swiftLanguageMode(.v5), @@ -326,8 +362,11 @@ let package = Package( ] ), .target( - name: "SwiftKitSwift", - dependencies: [], + name: "SwiftJavaRuntimeSupport", + dependencies: [ + "CSwiftJavaJNI", + "SwiftJava" + ], swiftSettings: [ .swiftLanguageMode(.v5), .unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"]) @@ -335,7 +374,7 @@ let package = Package( ), .target( - name: "JavaRuntime", + name: "SwiftRuntimeFunctions", swiftSettings: [ .swiftLanguageMode(.v5), .unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"]) @@ -343,27 +382,41 @@ let package = Package( ), .target( - name: "JavaKitConfigurationShared" + name: "CSwiftJavaJNI", + swiftSettings: [ + .swiftLanguageMode(.v5), + .unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"], .when(platforms: [.macOS, .linux, .windows])) + ], + linkerSettings: [ + .linkedLibrary("log", .when(platforms: [.android])) + ] + ), + + .target( + name: "SwiftJavaConfigurationShared" ), .target( - name: "JavaKitShared" + name: "SwiftJavaShared" ), .target( - name: "SwiftJavaLib", + name: "SwiftJavaToolLib", dependencies: [ + .product(name: "Logging", package: "swift-log"), + .product(name: "OrderedCollections", package: "swift-collections"), .product(name: "SwiftBasicFormat", package: "swift-syntax"), .product(name: "SwiftSyntax", package: "swift-syntax"), .product(name: "SwiftSyntaxBuilder", package: "swift-syntax"), - "JavaKit", - "JavaKitJar", - "JavaKitReflection", - "JavaKitNetwork", + "SwiftJava", + "JavaUtilJar", + "JavaLangReflect", + "JavaNet", "JavaTypes", - "JavaKitShared", - "JavaKitConfigurationShared", - "_Subprocess", // using process spawning + "SwiftJavaShared", + "SwiftJavaConfigurationShared", + // .product(name: "Subprocess", package: "swift-subprocess") + "_Subprocess", ], swiftSettings: [ .swiftLanguageMode(.v5), @@ -379,19 +432,23 @@ let package = Package( .product(name: "SwiftSyntax", package: "swift-syntax"), .product(name: "SwiftSyntaxBuilder", package: "swift-syntax"), .product(name: "ArgumentParser", package: "swift-argument-parser"), - "JavaKit", - "JavaKitJar", - "JavaKitNetwork", - "SwiftJavaLib", + .product(name: "SystemPackage", package: "swift-system"), + "SwiftJava", + "JavaUtilJar", + "JavaNet", + "SwiftJavaToolLib", "JExtractSwiftLib", - "JavaKitShared", - "JavaKitConfigurationShared", + "SwiftJavaShared", + "SwiftJavaConfigurationShared", ], - swiftSettings: [ .swiftLanguageMode(.v5), .unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"]), .enableUpcomingFeature("BareSlashRegexLiterals"), + .define( + "SYSTEM_PACKAGE_DARWIN", + .when(platforms: [.macOS, .macCatalyst, .iOS, .watchOS, .tvOS, .visionOS])), + .define("SYSTEM_PACKAGE"), ] ), @@ -399,12 +456,14 @@ let package = Package( name: "JExtractSwiftLib", dependencies: [ .product(name: "SwiftBasicFormat", package: "swift-syntax"), + .product(name: "SwiftLexicalLookup", package: "swift-syntax"), .product(name: "SwiftSyntax", package: "swift-syntax"), .product(name: "SwiftSyntaxBuilder", package: "swift-syntax"), .product(name: "ArgumentParser", package: "swift-argument-parser"), + .product(name: "OrderedCollections", package: "swift-collections"), "JavaTypes", - "JavaKitShared", - "JavaKitConfigurationShared", + "SwiftJavaShared", + "SwiftJavaConfigurationShared", ], swiftSettings: [ .swiftLanguageMode(.v5), @@ -419,20 +478,13 @@ let package = Package( "SwiftJavaTool" ] ), - .plugin( - name: "JExtractSwiftCommandPlugin", - capability: .command( - intent: .custom(verb: "jextract", description: "Extract Java accessors from Swift module"), - permissions: [ - ]), - dependencies: [ - "SwiftJavaTool" - ] - ), .testTarget( - name: "JavaKitTests", - dependencies: ["JavaKit", "JavaKitNetwork"], + name: "SwiftJavaTests", + dependencies: [ + "SwiftJava", + "JavaNet" + ], swiftSettings: [ .swiftLanguageMode(.v5), .unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"]) @@ -441,16 +493,18 @@ let package = Package( .testTarget( name: "JavaTypesTests", - dependencies: ["JavaTypes"], + dependencies: [ + "JavaTypes" + ], swiftSettings: [ .swiftLanguageMode(.v5) ] ), .testTarget( - name: "JavaKitMacroTests", + name: "SwiftJavaMacrosTests", dependencies: [ - "JavaKitMacros", + "SwiftJavaMacros", .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), ], swiftSettings: [ @@ -459,8 +513,19 @@ let package = Package( ), .testTarget( - name: "SwiftJavaTests", - dependencies: ["SwiftJavaLib"], + name: "SwiftJavaToolLibTests", + dependencies: [ + "SwiftJavaToolLib" + ], + swiftSettings: [ + .swiftLanguageMode(.v5), + .unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"]) + ] + ), + + .testTarget( + name: "SwiftJavaConfigurationSharedTests", + dependencies: ["SwiftJavaConfigurationShared"], swiftSettings: [ .swiftLanguageMode(.v5), .unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"]) @@ -480,7 +545,7 @@ let package = Package( // Experimental Foundation Subprocess Copy .target( - name: "_CShims", + name: "_SubprocessCShims", swiftSettings: [ .swiftLanguageMode(.v5) ] @@ -488,11 +553,15 @@ let package = Package( .target( name: "_Subprocess", dependencies: [ - "_CShims", + "_SubprocessCShims", .product(name: "SystemPackage", package: "swift-system"), ], swiftSettings: [ - .swiftLanguageMode(.v5) + .swiftLanguageMode(.v5), + .define( + "SYSTEM_PACKAGE_DARWIN", + .when(platforms: [.macOS, .macCatalyst, .iOS, .watchOS, .tvOS, .visionOS])), + .define("SYSTEM_PACKAGE"), ] ), ] diff --git a/Plugins/JExtractSwiftCommandPlugin/JExtractSwiftCommandPlugin.swift b/Plugins/JExtractSwiftCommandPlugin/JExtractSwiftCommandPlugin.swift deleted file mode 100644 index 3ea898862..000000000 --- a/Plugins/JExtractSwiftCommandPlugin/JExtractSwiftCommandPlugin.swift +++ /dev/null @@ -1,159 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift.org project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift.org project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import Foundation -import PackagePlugin - -@main -final class JExtractSwiftCommandPlugin: SwiftJavaPluginProtocol, BuildToolPlugin, CommandPlugin { - - var pluginName: String = "swift-java-command" - var verbose: Bool = getEnvironmentBool("SWIFT_JAVA_VERBOSE") - - /// Build the target before attempting to extract from it. - /// This avoids trying to extract from broken sources. - /// - /// You may disable this if confident that input targets sources are correct and there's no need to kick off a pre-build for some reason. - var buildInputs: Bool = true - - /// Build the target once swift-java sources have been generated. - /// This helps verify that the generated output is correct, and won't miscompile on the next build. - var buildOutputs: Bool = true - - func createBuildCommands(context: PluginContext, target: any Target) async throws -> [Command] { - // FIXME: This is not a build plugin but SwiftPM forces us to impleme the protocol anyway? rdar://139556637 - return [] - } - - func performCommand(context: PluginContext, arguments: [String]) throws { - // Plugin can't have dependencies, so we have some naive argument parsing instead: - self.verbose = arguments.contains("-v") || arguments.contains("--verbose") - - for target in context.package.targets { - guard getSwiftJavaConfigPath(target: target) != nil else { - log("[swift-java-command] Skipping jextract step: Missing swift-java.config for target '\(target.name)'") - continue - } - - do { - let extraArguments = arguments.filter { arg in - arg != "-v" && arg != "--verbose" - } - print("[swift-java-command] Extracting Java wrappers from target: '\(target.name)'...") - try performCommand(context: context, target: target, extraArguments: extraArguments) - } catch { - print("[swift-java-command] error: Failed to extract from target '\(target.name)': \(error)") - } - - print("[swift-java-command] Done.") - } - print("[swift-java-command] Generating sources: " + "done".green + ".") - } - - func prepareJExtractArguments(context: PluginContext, target: Target) throws -> [String] { - guard let sourceModule = target.sourceModule else { return [] } - - // Note: Target doesn't have a directoryURL counterpart to directory, - // so we cannot eliminate this deprecation warning. - let sourceDir = target.directory.string - - let configuration = try readConfiguration(sourceDir: "\(sourceDir)") - - var arguments: [String] = [ - "--input-swift", sourceDir, - "--module-name", sourceModule.name, - "--output-java", context.outputJavaDirectory.path(percentEncoded: false), - "--output-swift", context.outputSwiftDirectory.path(percentEncoded: false), - // TODO: "--build-cache-directory", ... - // Since plugins cannot depend on libraries we cannot detect what the output files will be, - // as it depends on the contents of the input files. Therefore we have to implement this as a prebuild plugin. - // We'll have to make up some caching inside the tool so we don't re-parse files which have not changed etc. - ] - // arguments.append(sourceDir) // TODO: we could do this shape maybe? to have the dirs last? - if let package = configuration?.javaPackage, !package.isEmpty { - ["--java-package", package] - } - - return arguments - } - - /// Perform the command on a specific target. - func performCommand(context: PluginContext, target: Target, extraArguments: [String]) throws { - guard let sourceModule = target.sourceModule else { return } - - if self.buildInputs { - // Make sure the target can builds properly - log("Pre-building target '\(target.name)' before extracting sources...") - let targetBuildResult = try self.packageManager.build(.target(target.name), parameters: .init()) - - guard targetBuildResult.succeeded else { - print("[swift-java-command] Build of '\(target.name)' failed: \(targetBuildResult.logText)") - return - } - } - - let arguments = try prepareJExtractArguments(context: context, target: target) - - try runExtract(context: context, target: target, arguments: arguments + extraArguments) - - if self.buildOutputs { - // Building the *products* since we need to build the dylib that contains our newly generated sources, - // so just building the target again would not be enough. We build all products which we affected using - // our source generation, which usually would be just a product dylib with our library. - // - // In practice, we'll always want to build after generating; either here, - // or via some other task before we run any Java code, calling into Swift. - log("Post-extract building products with target '\(target.name)'...") - for product in context.package.products where product.targets.contains(where: { $0.id == target.id }) { - log("Post-extract building product '\(product.name)'...") - let buildResult = try self.packageManager.build(.product(product.name), parameters: .init()) - - if buildResult.succeeded { - log("Post-extract build: " + "done".green + ".") - } else { - log("Post-extract build: " + "done".red + "!") - } - } - } - } - - func runExtract(context: PluginContext, target: Target, arguments: [String]) throws { - let process = Process() - process.executableURL = try context.tool(named: "SwiftJavaTool").url - process.arguments = arguments - - do { - log("Execute: \(process.executableURL!.absoluteURL.relativePath) \(arguments.joined(separator: " "))") - - try process.run() - process.waitUntilExit() - - assert(process.terminationStatus == 0, "Process failed with exit code: \(process.terminationStatus)") - } catch { - print("[swift-java-command] Failed to extract Java sources for target: '\(target.name); Error: \(error)") - } - } - -} - -// Mini coloring helper, since we cannot have dependencies we keep it minimal here -extension String { - var red: String { - "\u{001B}[0;31m" + "\(self)" + "\u{001B}[0;0m" - } - var green: String { - "\u{001B}[0;32m" + "\(self)" + "\u{001B}[0;0m" - } -} - diff --git a/Plugins/JExtractSwiftCommandPlugin/_PluginsShared b/Plugins/JExtractSwiftCommandPlugin/_PluginsShared deleted file mode 120000 index de623a5ee..000000000 --- a/Plugins/JExtractSwiftCommandPlugin/_PluginsShared +++ /dev/null @@ -1 +0,0 @@ -../PluginsShared \ No newline at end of file diff --git a/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift b/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift index a2cda3521..2808b6d31 100644 --- a/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift +++ b/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift @@ -15,6 +15,8 @@ import Foundation import PackagePlugin +fileprivate let SwiftJavaConfigFileName = "swift-java.config" + @main struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin { @@ -23,9 +25,12 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin { func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] { let toolURL = try context.tool(named: "SwiftJavaTool").url - + + var commands: [Command] = [] + guard let sourceModule = target.sourceModule else { return [] } + // Note: Target doesn't have a directoryURL counterpart to directory, // so we cannot eliminate this deprecation warning. for dependency in target.dependencies { @@ -40,6 +45,10 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin { } let sourceDir = target.directory.string + + // The name of the configuration file SwiftJava.config from the target for + // which we are generating Swift wrappers for Java classes. + let configFile = URL(filePath: sourceDir).appending(path: "swift-java.config") let configuration = try readConfiguration(sourceDir: "\(sourceDir)") guard let javaPackage = configuration?.javaPackage else { @@ -47,37 +56,321 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin { log("Skipping jextract step, no 'javaPackage' configuration in \(getSwiftJavaConfigPath(target: target) ?? "")") return [] } - + // We use the the usual maven-style structure of "src/[generated|main|test]/java/..." // that is common in JVM ecosystem let outputJavaDirectory = context.outputJavaDirectory let outputSwiftDirectory = context.outputSwiftDirectory + let dependentConfigFiles = searchForDependentConfigFiles(in: target) + var arguments: [String] = [ + /*subcommand=*/"jextract", + "--swift-module", sourceModule.name, "--input-swift", sourceDir, - "--module-name", sourceModule.name, "--output-java", outputJavaDirectory.path(percentEncoded: false), "--output-swift", outputSwiftDirectory.path(percentEncoded: false), + // since SwiftPM requires all "expected" files do end up being written + // and we don't know which files will have actual thunks generated... we force jextract to write even empty files. + "--write-empty-files", // TODO: "--build-cache-directory", ... // Since plugins cannot depend on libraries we cannot detect what the output files will be, // as it depends on the contents of the input files. Therefore we have to implement this as a prebuild plugin. // We'll have to make up some caching inside the tool so we don't re-parse files which have not changed etc. ] - // arguments.append(sourceDir) + + let dependentConfigFilesArguments = dependentConfigFiles.flatMap { moduleAndConfigFile in + let (moduleName, configFile) = moduleAndConfigFile + return [ + "--depends-on", + "\(moduleName)=\(configFile.path(percentEncoded: false))" + ] + } + arguments += dependentConfigFilesArguments + if !javaPackage.isEmpty { - arguments.append(contentsOf: ["--java-package", javaPackage]) + arguments += ["--java-package", javaPackage] + } + + let swiftFiles = sourceModule.sourceFiles.map { $0.url }.filter { + $0.pathExtension == "swift" + } + + // Output Swift files are just Java filename based converted to Swift files one-to-one + var outputSwiftFiles: [URL] = swiftFiles.compactMap { sourceFileURL in + guard sourceFileURL.isFileURL else { + return nil as URL? + } + + let sourceFilePath = sourceFileURL.path + guard sourceFilePath.starts(with: sourceDir) else { + fatalError("Could not get relative path for source file \(sourceFilePath)") + } + let outputURL = outputSwiftDirectory + .appending(path: String(sourceFilePath.dropFirst(sourceDir.count).dropLast(sourceFileURL.lastPathComponent.count + 1))) + + let inputFileName = sourceFileURL.deletingPathExtension().lastPathComponent + return outputURL.appending(path: "\(inputFileName)+SwiftJava.swift") + } + + // Append the "module file" that contains any thunks for global func definitions + outputSwiftFiles += [ + outputSwiftDirectory.appending(path: "\(sourceModule.name)Module+SwiftJava.swift") + ] + + // If the module uses 'Data' type, the thunk file is emitted as if 'Data' is declared + // in that module. Declare the thunk file as the output. + outputSwiftFiles += [ + outputSwiftDirectory.appending(path: "Foundation+SwiftJava.swift") + ] + + print("[swift-java-plugin] Output swift files:\n - \(outputSwiftFiles.map({$0.absoluteString}).joined(separator: "\n - "))") + + var jextractOutputFiles = outputSwiftFiles + + // If the developer has enabled java callbacks in the configuration (default is false) + // and we are running in JNI mode, we will run additional phases in this build plugin + // to generate Swift wrappers using wrap-java that can be used to callback to Java. + let shouldRunJavaCallbacksPhases = + if let configuration, + configuration.enableJavaCallbacks == true, + configuration.effectiveMode == .jni { + true + } else { + false + } + + // Extract list of all sources + let javaSourcesListFileName = "jextract-generated-sources.txt" + let javaSourcesFile = outputJavaDirectory.appending(path: javaSourcesListFileName) + if shouldRunJavaCallbacksPhases { + arguments += [ + "--generated-java-sources-list-file-output", javaSourcesListFileName + ] + jextractOutputFiles += [javaSourcesFile] } - return [ - .prebuildCommand( + commands += [ + .buildCommand( displayName: "Generate Java wrappers for Swift types", executable: toolURL, arguments: arguments, - // inputFiles: [ configFile ] + swiftFiles, - // outputFiles: outputJavaFiles - outputFilesDirectory: outputSwiftDirectory + inputFiles: [ configFile ] + swiftFiles, + outputFiles: jextractOutputFiles + ) + ] + + // If we do not need Java callbacks, we can skip the remaining steps. + guard shouldRunJavaCallbacksPhases else { + return commands + } + + // The URL of the compiled Java sources + let javaCompiledClassesURL = context.pluginWorkDirectoryURL + .appending(path: "compiled-java-output") + + // Build SwiftKitCore and get the classpath + // as the jextracted sources will depend on that + + guard let swiftJavaDirectory = findSwiftJavaDirectory(for: target) else { + fatalError("Unable to find the path to the swift-java sources, please file an issue.") + } + log("Found swift-java at \(swiftJavaDirectory)") + + let swiftKitCoreClassPath = swiftJavaDirectory.appending(path: "SwiftKitCore/build/classes/java/main") + + // We need to use a different gradle home, because + // this plugin might be run from inside another gradle task + // and that would cause conflicts. + let gradleUserHome = context.pluginWorkDirectoryURL.appending(path: "gradle-user-home") + + let GradleUserHome = "GRADLE_USER_HOME" + let gradleUserHomePath = gradleUserHome.path(percentEncoded: false) + log("Prepare command: :SwiftKitCore:build in \(GradleUserHome)=\(gradleUserHomePath)") + var gradlewEnvironment = ProcessInfo.processInfo.environment + gradlewEnvironment[GradleUserHome] = gradleUserHomePath + log("Forward environment: \(gradlewEnvironment)") + + let gradleExecutable = findExecutable(name: "gradle") ?? // try using installed 'gradle' if available in PATH + swiftJavaDirectory.appending(path: "gradlew") // fallback to calling ./gradlew if gradle is not installed + log("Detected 'gradle' executable (or gradlew fallback): \(gradleExecutable)") + + commands += [ + .buildCommand( + displayName: "Build SwiftKitCore using Gradle (Java)", + executable: gradleExecutable, + arguments: [ + ":SwiftKitCore:build", + "--project-dir", swiftJavaDirectory.path(percentEncoded: false), + "--gradle-user-home", gradleUserHomePath, + "--configure-on-demand", + "--no-daemon" + ], + environment: gradlewEnvironment, + inputFiles: [swiftJavaDirectory], + outputFiles: [swiftKitCoreClassPath] + ) + ] + + // Compile the jextracted sources + let javaHome = URL(filePath: findJavaHome()) + + commands += [ + .buildCommand( + displayName: "Build extracted Java sources", + executable: javaHome + .appending(path: "bin") + .appending(path: self.javacName), + arguments: [ + "@\(javaSourcesFile.path(percentEncoded: false))", + "-d", javaCompiledClassesURL.path(percentEncoded: false), + "-parameters", + "-classpath", swiftKitCoreClassPath.path(percentEncoded: false) + ], + inputFiles: [javaSourcesFile, swiftKitCoreClassPath], + outputFiles: [javaCompiledClassesURL] + ) + ] + + // Run `configure` to extract a swift-java config to use for wrap-java + let swiftJavaConfigURL = context.pluginWorkDirectoryURL.appending(path: "swift-java.config") + + commands += [ + .buildCommand( + displayName: "Output swift-java.config that contains all extracted Java sources", + executable: toolURL, + arguments: [ + "configure", + "--output-directory", context.pluginWorkDirectoryURL.path(percentEncoded: false), + "--cp", javaCompiledClassesURL.path(percentEncoded: false), + "--swift-module", sourceModule.name, + "--swift-type-prefix", "Java" + ], + inputFiles: [javaCompiledClassesURL], + outputFiles: [swiftJavaConfigURL] + ) + ] + + let singleSwiftFileOutputName = "WrapJavaGenerated.swift" + + // In the end we can run wrap-java on the previous inputs + var wrapJavaArguments = [ + "wrap-java", + "--swift-module", sourceModule.name, + "--output-directory", outputSwiftDirectory.path(percentEncoded: false), + "--config", swiftJavaConfigURL.path(percentEncoded: false), + "--cp", swiftKitCoreClassPath.path(percentEncoded: false), + "--single-swift-file-output", singleSwiftFileOutputName + ] + + // Add any dependent config files as arguments + wrapJavaArguments += dependentConfigFilesArguments + + commands += [ + .buildCommand( + displayName: "Wrap compiled Java sources using wrap-java", + executable: toolURL, + arguments: wrapJavaArguments, + inputFiles: [swiftJavaConfigURL, swiftKitCoreClassPath], + outputFiles: [outputSwiftDirectory.appending(path: singleSwiftFileOutputName)] ) ] + + return commands + } + + var javacName: String { +#if os(Windows) + "javac.exe" +#else + "javac" +#endif + } + + /// Find the manifest files from other swift-java executions in any targets + /// this target depends on. + func searchForDependentConfigFiles(in target: any Target) -> [(String, URL)] { + var dependentConfigFiles = [(String, URL)]() + + func _searchForConfigFiles(in target: any Target) { + // log("Search for config files in target: \(target.name)") + let dependencyURL = URL(filePath: target.directory.string) + + // Look for a config file within this target. + let dependencyConfigURL = dependencyURL + .appending(path: SwiftJavaConfigFileName) + let dependencyConfigString = dependencyConfigURL + .path(percentEncoded: false) + + if FileManager.default.fileExists(atPath: dependencyConfigString) { + dependentConfigFiles.append((target.name, dependencyConfigURL)) + } + } + + // Process direct dependencies of this target. + for dependency in target.dependencies { + switch dependency { + case .target(let target): + // log("Dependency target: \(target.name)") + _searchForConfigFiles(in: target) + + case .product(let product): + // log("Dependency product: \(product.name)") + for target in product.targets { + // log("Dependency product: \(product.name), target: \(target.name)") + _searchForConfigFiles(in: target) + } + + @unknown default: + break + } + } + + // Process indirect target dependencies. + for dependency in target.recursiveTargetDependencies { + // log("Recursive dependency target: \(dependency.name)") + _searchForConfigFiles(in: dependency) + } + + return dependentConfigFiles + } + + private func findSwiftJavaDirectory(for target: any Target) -> URL? { + for dependency in target.dependencies { + switch dependency { + case .target(let target): + continue + + case .product(let product): + guard let swiftJava = product.sourceModules.first(where: { $0.name == "SwiftJava" }) else { + return nil + } + + // We are inside Sources/SwiftJava + return swiftJava.directoryURL.deletingLastPathComponent().deletingLastPathComponent() + + @unknown default: + continue + } + } + + return nil } } +func findExecutable(name: String) -> URL? { + let fileManager = FileManager.default + + guard let path = ProcessInfo.processInfo.environment["PATH"] else { + return nil + } + + for path in path.split(separator: ":") { + let fullURL = URL(fileURLWithPath: String(path)).appendingPathComponent(name) + if fileManager.isExecutableFile(atPath: fullURL.path) { + return fullURL + } + } + + return nil +} \ No newline at end of file diff --git a/Plugins/JavaCompilerPlugin/JavaCompilerPlugin.swift b/Plugins/JavaCompilerPlugin/JavaCompilerPlugin.swift index b93fe403a..93c358df0 100644 --- a/Plugins/JavaCompilerPlugin/JavaCompilerPlugin.swift +++ b/Plugins/JavaCompilerPlugin/JavaCompilerPlugin.swift @@ -30,50 +30,61 @@ struct JavaCompilerBuildToolPlugin: BuildToolPlugin { // Note: Target doesn't have a directoryURL counterpart to directory, // so we cannot eliminate this deprecation warning. - let sourceDir = target.directory.string + let sourceDir = URL(filePath: target.directory.string) - // The name of the configuration file JavaKit.config from the target for + // The name of the configuration file SwiftJava.config from the target for // which we are generating Swift wrappers for Java classes. - let configFile = URL(filePath: sourceDir).appending(path: "Java2Swift.config") + let configFile = sourceDir.appending(path: "swift-java.config") let config: Configuration? if let configData = try? Data(contentsOf: configFile) { - config = try? JSONDecoder().decode(Configuration.self, from: configData) + let decoder = JSONDecoder() + decoder.allowsJSON5 = true + config = try? decoder.decode(Configuration.self, from: configData) } else { config = nil } // The class files themselves will be generated into the build directory // for this target. - let classFiles = javaFiles.map { sourceFileURL in + let classFiles = javaFiles.compactMap { sourceFileURL in + guard sourceFileURL.isFileURL else { + return nil as URL? + } + let sourceFilePath = sourceFileURL.path - guard sourceFilePath.starts(with: sourceDir) else { + let sourceDirPath = sourceDir.path + guard sourceFilePath.starts(with: sourceDirPath) else { fatalError("Could not get relative path for source file \(sourceFilePath)") } - return URL( - filePath: context.pluginWorkDirectoryURL.path - ).appending(path: "Java") - .appending(path: String(sourceFilePath.dropFirst(sourceDir.count))) - .deletingPathExtension() - .appendingPathExtension("class") + return URL(filePath: context.pluginWorkDirectoryURL.path) + .appending(path: "Java") + .appending(path: String(sourceFilePath.dropFirst(sourceDirPath.count))) + .deletingPathExtension() + .appendingPathExtension("class") } let javaHome = URL(filePath: findJavaHome()) let javaClassFileURL = context.pluginWorkDirectoryURL .appending(path: "Java") + #if os(Windows) + let javac = "javac.exe" + #else + let javac = "javac" + #endif return [ .buildCommand( displayName: "Compiling \(javaFiles.count) Java files for target \(sourceModule.name) to \(javaClassFileURL)", executable: javaHome .appending(path: "bin") - .appending(path: "javac"), - arguments: javaFiles.map { $0.path(percentEncoded: false) } + [ - "-d", javaClassFileURL.path(), + .appending(path: javac), + arguments: javaFiles.map { $0.path } + [ + "-d", javaClassFileURL.path, "-parameters", // keep parameter names, which allows us to emit them in generated Swift decls ] + (config?.compilerVersionArgs ?? []), inputFiles: javaFiles, - outputFiles: classFiles + outputFiles: classFiles // FIXME: this is not quite enough, javac may generate more files for closures etc, which we don't know about unless we compile first ) ] } diff --git a/Plugins/PluginsShared/JavaKitConfigurationShared b/Plugins/PluginsShared/JavaKitConfigurationShared deleted file mode 120000 index d5c765dfc..000000000 --- a/Plugins/PluginsShared/JavaKitConfigurationShared +++ /dev/null @@ -1 +0,0 @@ -../../Sources/JavaKitConfigurationShared \ No newline at end of file diff --git a/Plugins/PluginsShared/PluginUtils.swift b/Plugins/PluginsShared/PluginUtils.swift index 691d33759..8278c6455 100644 --- a/Plugins/PluginsShared/PluginUtils.swift +++ b/Plugins/PluginsShared/PluginUtils.swift @@ -66,14 +66,14 @@ extension PluginContext { .appending(path: "generated") .appending(path: "java") } - + var outputSwiftDirectory: URL { self.pluginWorkDirectoryURL .appending(path: "Sources") } - - func cachedClasspathFile(moduleName: String) -> URL { + + func cachedClasspathFile(swiftModule: String) -> URL { self.pluginWorkDirectoryURL - .appending(path: "\(moduleName)", directoryHint: .notDirectory) + .appending(path: "\(swiftModule)", directoryHint: .notDirectory) } } diff --git a/Plugins/PluginsShared/SwiftJavaConfigurationShared b/Plugins/PluginsShared/SwiftJavaConfigurationShared new file mode 120000 index 000000000..2af5fd01e --- /dev/null +++ b/Plugins/PluginsShared/SwiftJavaConfigurationShared @@ -0,0 +1 @@ +../../Sources/SwiftJavaConfigurationShared \ No newline at end of file diff --git a/Plugins/PluginsShared/SwiftJavaPluginProtocol.swift b/Plugins/PluginsShared/SwiftJavaPluginProtocol.swift index 68f8964e5..57e38ef7a 100644 --- a/Plugins/PluginsShared/SwiftJavaPluginProtocol.swift +++ b/Plugins/PluginsShared/SwiftJavaPluginProtocol.swift @@ -21,8 +21,6 @@ protocol SwiftJavaPluginProtocol { extension SwiftJavaPluginProtocol { func log(_ message: @autoclosure () -> String, terminator: String = "\n") { -// if self.verbose { - print("[\(pluginName)] \(message())", terminator: terminator) -// } + print("[\(pluginName)] \(message())", terminator: terminator) } } diff --git a/Plugins/SwiftJavaPlugin/SwiftJavaPlugin.swift b/Plugins/SwiftJavaPlugin/SwiftJavaPlugin.swift index 77d7058dd..a578091f1 100644 --- a/Plugins/SwiftJavaPlugin/SwiftJavaPlugin.swift +++ b/Plugins/SwiftJavaPlugin/SwiftJavaPlugin.swift @@ -34,7 +34,7 @@ struct SwiftJavaBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin { // so we cannot eliminate this deprecation warning. let sourceDir = target.directory.string - // The name of the configuration file JavaKit.config from the target for + // The name of the configuration file SwiftJava.config from the target for // which we are generating Swift wrappers for Java classes. let configFile = URL(filePath: sourceDir) .appending(path: SwiftJavaConfigFileName) @@ -44,7 +44,7 @@ struct SwiftJavaBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin { log("Config was: \(config)") var javaDependencies = config.dependencies ?? [] - /// Find the manifest files from other Java2Swift executions in any targets + /// Find the manifest files from other swift-java executions in any targets /// this target depends on. var dependentConfigFiles: [(String, URL)] = [] func searchForConfigFiles(in target: any Target) { @@ -88,26 +88,14 @@ struct SwiftJavaBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin { } var arguments: [String] = [] - arguments += argumentsModuleName(sourceModule: sourceModule) + arguments += argumentsSwiftModule(sourceModule: sourceModule) arguments += argumentsOutputDirectory(context: context) - - arguments += dependentConfigFiles.flatMap { moduleAndConfigFile in - let (moduleName, configFile) = moduleAndConfigFile - return [ - "--depends-on", - "\(moduleName)=\(configFile.path(percentEncoded: false))" - ] - } - arguments.append(configFile.path(percentEncoded: false)) + arguments += argumentsDependedOnConfigs(dependentConfigFiles) -// guard let classes = config.classes else { -// log("Config at \(configFile) did not have 'classes' configured, skipping java2swift step.") -// return [] -// } let classes = config.classes ?? [:] - print("Classes to wrap: \(classes.map(\.key))") + print("[swift-java-plugin] Classes to wrap (\(classes.count)): \(classes.map(\.key))") - /// Determine the set of Swift files that will be emitted by the Java2Swift tool. + /// Determine the set of Swift files that will be emitted by the swift-java tool. // TODO: this is not precise and won't work with more advanced Java files, e.g. lambdas etc. let outputDirectoryGenerated = self.outputDirectory(context: context, generated: true) let outputSwiftFiles = classes.map { (javaClassName, swiftName) in @@ -165,11 +153,9 @@ struct SwiftJavaBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin { .buildCommand( displayName: displayName, executable: executable, - arguments: [ - "--fetch", configFile.path(percentEncoded: false), - "--module-name", sourceModule.name, - "--output-directory", outputDirectory(context: context, generated: false).path(percentEncoded: false) - ], + arguments: ["resolve"] + + argumentsOutputDirectory(context: context, generated: false) + + argumentsSwiftModule(sourceModule: sourceModule), environment: [:], inputFiles: [configFile], outputFiles: fetchDependenciesOutputFiles @@ -179,40 +165,67 @@ struct SwiftJavaBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin { log("No dependencies to fetch for target \(sourceModule.name)") } + // Add all the core Java stdlib modules as --depends-on + let javaStdlibModules = getExtractedJavaStdlibModules() + log("Include Java standard library SwiftJava modules: \(javaStdlibModules)") + arguments += javaStdlibModules.flatMap { ["--depends-on", $0] } + if !outputSwiftFiles.isEmpty { + let displayName = "Wrapping \(classes.count) Java classes in Swift target '\(sourceModule.name)'" + log("Prepared: \(displayName)") commands += [ .buildCommand( - displayName: "Wrapping \(classes.count) Java classes in Swift target '\(sourceModule.name)'", + displayName: displayName, executable: executable, - arguments: arguments, - inputFiles: compiledClassFiles + fetchDependenciesOutputFiles + [ - configFile - ], + arguments: ["wrap-java"] + + arguments, + inputFiles: compiledClassFiles + fetchDependenciesOutputFiles + [ configFile ], outputFiles: outputSwiftFiles ) ] - } else { - log("No Swift output files, skip wrapping") } - + + if commands.isEmpty { + log("No swift-java commands for module '\(sourceModule.name)'") + } + return commands } } extension SwiftJavaBuildToolPlugin { - func argumentsModuleName(sourceModule: Target) -> [String] { + func argumentsSwiftModule(sourceModule: Target) -> [String] { return [ - "--module-name", sourceModule.name + "--swift-module", sourceModule.name ] } - + + // FIXME: remove this and the deprecated property inside SwiftJava, this is a workaround + // since we cannot have the same option in common options and in the top level + // command from which we get into sub commands. The top command will NOT have this option. + func argumentsSwiftModuleDeprecated(sourceModule: Target) -> [String] { + return [ + "--swift-module-deprecated", sourceModule.name + ] + } + func argumentsOutputDirectory(context: PluginContext, generated: Bool = true) -> [String] { return [ "--output-directory", outputDirectory(context: context, generated: generated).path(percentEncoded: false) ] } - + + func argumentsDependedOnConfigs(_ dependentConfigFiles: [(String, URL)]) -> [String] { + dependentConfigFiles.flatMap { moduleAndConfigFile in + let (moduleName, configFile) = moduleAndConfigFile + return [ + "--depends-on", + "\(moduleName)=\(configFile.path(percentEncoded: false))" + ] + } + } + func outputDirectory(context: PluginContext, generated: Bool = true) -> URL { let dir = context.pluginWorkDirectoryURL if generated { @@ -226,3 +239,29 @@ extension SwiftJavaBuildToolPlugin { outputDirectory(context: context, generated: generated).appending(path: filename) } } + +func getExtractedJavaStdlibModules() -> [String] { + let fileManager = FileManager.default + let sourcesPath = URL(fileURLWithPath: #filePath) + .deletingLastPathComponent() + .deletingLastPathComponent() + .appendingPathComponent("Sources") + .appendingPathComponent("JavaStdlib") + + guard let stdlibDirContents = try? fileManager.contentsOfDirectory( + at: sourcesPath, + includingPropertiesForKeys: [.isDirectoryKey], + options: [.skipsHiddenFiles] + ) else { + return [] + } + + return stdlibDirContents.compactMap { url in + guard let resourceValues = try? url.resourceValues(forKeys: [.isDirectoryKey]), + let isDirectory = resourceValues.isDirectory, + isDirectory else { + return nil + } + return url.lastPathComponent + }.sorted() +} diff --git a/README.md b/README.md index b7ad4defb..7494366e2 100644 --- a/README.md +++ b/README.md @@ -1,79 +1,178 @@ # Swift Java Interoperability Tools and Libraries -This repository contains two approaches to Swift/Java interoperability. +This project contains tools and libraries that facilitate **Swift & Java Interoperability**. -- A Swift library (`JavaKit`) and bindings generator that allows a Swift program to make use of Java libraries by wrapping Java classes in corresponding Swift types, allowing Swift to directly call any wrapped Java API. -- The `jextract-swift` tool which is similar to the JDK's `jextract` which allows to extract Java sources which are used - to efficiently call into Swift _from Java_. +- Swift library (`SwiftJava`) and bindings generator that allows a Swift program to make use of Java libraries by wrapping Java classes in corresponding Swift types, allowing Swift to directly call any wrapped Java API. +- The `swift-java` tool which offers automated ways to import or "extract" bindings to sources or libraries in either language. The results are bindings for Swift or Java. -## :construction: :construction: :construction: Early Development :construction: :construction: :construction: +## Introduction -**:construction: :construction: :construction: This is a *very early* prototype and everything is subject to change. :construction: :construction: :construction:** +If you'd like to check out a quick introduction to Swift & Java interoperability, you may be interested in this presentation from WWDC25: [WWDC25: Explore Swift and Java interoperability](https://www.youtube.com/watch?v=QSHO-GUGidA). -Parts of this project are incomplete, not fleshed out, and subject to change without any notice. - -The primary purpose of this repository is to create an environment for collaboration and joint exploration of the Swift/Java interoperability story. The project will transition to a more structured approach once key goals have been outlined. +While we work on more quickstarts and documentation, please refer to the Sample projects located in [Samples/](Samples/) that showcase the various ways you can use swift-java in your Swift or Java projects. ## Dependencies -### Required Swift Development Toolchains +### Required JDK versions + +Note that this project consists of multiple modules which currently have different Swift and Java runtime requirements. -To build and use this project, currently, you will need to download a custom toolchain which includes some improvements in Swift that this project relies on: +You'll need to install the necessary JDK version locally. On macOS for example, you can install the JDK with [homebrew](https://brew.sh) using: -**Required toolchain download:** +```bash +$ brew install openjdk +# and create a symlink into /Library/Java/JavaVirtualMachines +$ sudo ln -sfn /opt/homebrew/opt/openjdk/libexec/openjdk.jdk /Library/Java/JavaVirtualMachines/openjdk.jdk -Currently this project supports Swift `6.0.x` and we are working on supporting later releases. +# or if you have a distribution as cask it will be installed into /Library/Java/JavaVirtualMachines +$ brew install --cask corretto +``` -You can use Swiftly ([macOS](https://www.swift.org/install/macos/swiftly/) / [linux](https://www.swift.org/install/linux/swiftly/)) the Swift toolchain installer to install the necessary Swift versions. +Alternatively, you can use a JDK manager like [sdkman](https://sdkman.io/install/) and set your `JAVA_HOME` environment variable: -### Required JDK versions +```bash +$ export JAVA_HOME="$(sdk home java current)" +``` + +E.g sdkman install command: + +```bash +sdk install java 25.0.1-amzn +``` + +## Self-publish supporting Java libraries + +Swift-java relies on supporting libraries that are under active development and not yet published to Maven Central. To use the project, you'll need to self-publish these libraries locally so your Java project can depend on them. + +To publish the libraries to your local maven repository (`$HOME/.m2`), you can run: + +``` +// in swift-java/ +./gradlew publishToMavenLocal +``` + +To consume these libraries in your Java project built using Gradle, you can then include the local repository in the repositories to resolve dependencies from: + +``` +repositories { + mavenLocal() + mavenCentral() +} +``` + +We anticipate simplifying this in the future. + +## SwiftJava macros -This project consists of different modules which have different Swift and Java runtime requirements. +SwiftJava is a Swift library offering macros which simplify writing JNI code "by hand" but also calling Java code from Swift. -**JavaKit** – the Swift macros allowing the invocation of Java libraries from Swift +It is possible to generate Swift bindings to Java libraries using SwiftJava by using the `swift-java wrap-java` command. -- **JDK 17+**, any recent JDK installation should be sufficient, as only general reflection and JNI APIs are used by this integration -- **Swift 6.0.x**, because the library uses modern Swift macros +Required language/runtime versions: +- **JDK 17+**, any recent JDK installation should be sufficient, as only general reflection and JNI APIs are used by this integratio +- **Swift 6.2.x**, because the library uses modern Swift macros -**jextract-swift** – the source generator that ingests .swiftinterface files and makes them available to be called from generated Java sources +**swift-java jextract** -- **Swift 6.0.x development snapshots**, because of dependence on rich swift interface files -- **JDK 22+** because of dependence on [JEP-454: Foreign Function & Memory API](https://openjdk.org/jeps/454) - - We are validating the implementation using the currently supported non-LTE release, which at present means JDK-23. +Is a source generator which will **generate Java bindings to existing Swift libraries**. +Its inputs are Swift sources or packages, and outputs are generated Swift and Java code necessary to call these functions efficiently from Java. -The extract tool may become able to generate legacy compatible sources, which would not require JEP-454 and would instead rely on existing JNI facilities. Currently though, efforts are focused on the forward-looking implementation using modern foreign function and memory APIs. +## swift-java jextract --mode=ffm (default) + +This mode provides the most flexibility and performance, and allows to decrease the amount of data being copied between Swift and Java. +This does require the use of the relatively recent [JEP-454: Foreign Function & Memory API](https://openjdk.org/jeps/454), which is only available since JDK22, and will become part of JDK LTS releases with JDK 25 (depending on your JDK vendor). + +This is the primary way we envision calling Swift code from server-side Java libraries and applications. + +Required language/runtime versions: +- **Swift 6.2**, because of dependence on rich swift interface files +- **JDK 25+** + - We are validating the implementation using the currently supported non-LTE release, which at present means JDK-25. + +## swift-java jextract --mode=jni + +In this mode, the generated sources will use the legacy JNI approach to calling native code. + +This mode is more limited in some performance and flexibility that it can offer, however it is the most compatible, since even very old JVM's as well as even Android systems can be supported by this mode. +We recommend this mode when FFM is not available, or wide ranging deployment compatibility is your priority. When performance is paramaunt, we recommend the FFM mode instead. + +Required language/runtime versions: +- **Swift 6.2**, because of dependence on rich swift interface files +- **Java 7+**, including -Support for more recent Swift versions will be provided, for now please stick to 6.0 while evaluating this early version of swift-java. ## Development and Testing This project contains multiple builds, living side by side together. -Depending on which part you are developing, you may want to run just the swift tests: +You will need to have: +- Swift (6.2.x+) +- Java (25+ for FFM, even though we support lower JDK targets) +- Gradle (installed by "Gradle wrapper" automatically when you run gradle through `./gradlew`) + +### Preparing your environment + +Install **Swift**, the easiest way to do this is to use **Swiftly**: [swift.org/install/](https://www.swift.org/install/). +This should automatically install a recent Swift, but you can always make sure by running: + +```bash +swiftly install 6.2 --use +``` + +Install a recent enough Java distribution. We validate this project using Corretto so you can choose to use that as well, +however any recent enough Java distribution should work correctly. You can use sdkman to install Java: + +```bash +# Install sdkman from: https://sdkman.io +curl -s "https://get.sdkman.io" | bash +sdk install java 17.0.15-amzn +sdk install java 25.0.1-amzn + +sdk use java 25.0.1-amzn +``` + +The use of JDK 25 is required to build the project, even though the libraries being published may target lower Java versions. + +❗️ Please make sure to `export JAVA_HOME` such that swift-java can find the necessary java libraries! +When using sdkman the easiest way to export JAVA_HOME is to export the "current" used JDK's home, like this: + +```bash +export JAVA_HOME="$(sdk home java current)" +``` + +### Testing your changes + +Many tests, including source generation tests, are written in Swift and you can execute them all by running the +swift package manager test command: ```bash > swift test ``` -or the Java tests through the Gradle build. The Gradle build may also trigger some Swift compilation because of -interlinked dependencies of the two parts of Swift-Java. To run the Java build and tests use the Gradle wrapper script: +When adding tests in `Tests/...` targets, you can run these tests (or filter a specific test using `swift test --filter type-or-method-name`). + +Some tests are implemented in Java and therefore need to be executed using Gradle. +Please always use the gradle wrapper (`./gradlew`) to make sure to use the appropriate Gradle version ```bash > ./gradlew test ``` -Currently it is suggested to use Swift 6.0 and a Java 24+. +> Tip: A lot of the **runtime tests** for code relying on `jextract` are **located in sample apps**, +> so if you need to runtime test any code relying on source generation steps of jextract, consider adding the tests +> to an appropriate Sample. These tests are also executed in CI (which you can check in the `ci-validate.sh` script +> contained in every sample repository). -### Sample Apps +### Sample apps & tests Sample apps are located in the `Samples/` directory, and they showcase full "roundtrip" usage of the library and/or tools. -#### JavaKit (Swift -> Java) +#### SwiftJava (Swift -> Java) To run a simple app showcasing a Swift process calling into a Java library you can run: ```bash -cd Samples/JavaKitSampleApp +cd Samples/SwiftJavaExtractFFMSampleApp ./ci-validate.sh # which is just `swift build` and a `java -cp ...` invocation of the compiled program ``` @@ -82,17 +181,23 @@ cd Samples/JavaKitSampleApp To run a simple example app showcasing the jextract (Java calling Swift) approach you can: ```bash -./gradlew Samples:SwiftKitSampleApp:run +./gradlew Samples:SwiftJavaExtractFFMSampleApp:run ``` This will also generate the necessary sources (by invoking jextract, extracting the `Sources/ExampleSwiftLibrary`) and generating Java sources in `src/generated/java`. +#### Other sample apps + +Please refer to the [Samples](Samples) directory for more sample apps which showcase the various usage modes of swift-java. + ## Benchmarks You can run Swift [ordo-one/package-benchmark](https://github.com/ordo-one/package-benchmark) and OpenJDK [JMH](https://github.com/openjdk/jmh) benchmarks in this project. -Swift benchmarks are located under `Benchmarks/` and JMH benchmarks are currently part of the SwiftKit sample project: `Samples/SwiftKitSampleApp/src/jmh` because they depend on generated sources from the sample. +Swift benchmarks are located under `Benchmarks/` and JMH benchmarks are currently part of the SwiftKit sample project: `Samples/SwiftJavaExtractFFMSampleApp/src/jmh` because they depend on generated sources from the sample. + +### Swift benchmarks To run **Swift benchmarks** you can: @@ -101,15 +206,39 @@ cd Benchmarks swift package benchmark ``` +### Java benchmarks + In order to run JMH benchmarks you can: ```bash -cd Samples/SwiftKitSampleApp -gradle jmh +cd Samples/SwiftJavaExtractFFMSampleApp +./gradlew jmh ``` Please read documentation of both performance testing tools and understand that results must be interpreted and not just taken at face value. Benchmarking is tricky and environment sensitive task, so please be careful when constructing and reading benchmarks and their results. If in doubt, please reach out on the forums. ## User Guide -More details about the project and how it can be used are available in [USER_GUIDE.md](USER_GUIDE.md) +More details about the project can be found in [docc](https://www.swift.org/documentation/docc/) documentation. + +To view the rendered docc documentation you can use the docc preview command: + +```bash +xcrun docc preview Sources/SwiftJavaDocumentation/Documentation.docc + +# OR SwiftJava to view SwiftJava documentation: +# xcrun docc preview Sources/SwiftJava/Documentation.docc + +# ======================================== +# Starting Local Preview Server +# Address: http://localhost:8080/documentation/documentation +# ======================================== +# Monitoring /Users/ktoso/code/swift-java/Sources/SwiftJavaDocumentation/Documentation.docc for changes... + +``` + +## Project Status + +**This project is under active development. We welcome feedback about any issues you encounter.** + +There is no guarantee about API stability until the project reaches a 1.0 release. diff --git a/Samples/JavaDependencySampleApp/Package.swift b/Samples/JavaDependencySampleApp/Package.swift index b39e7b811..573a60cc2 100644 --- a/Samples/JavaDependencySampleApp/Package.swift +++ b/Samples/JavaDependencySampleApp/Package.swift @@ -43,11 +43,10 @@ let javaIncludePath = "\(javaHome)/include" let package = Package( name: "JavaDependencySampleApp", platforms: [ - .macOS(.v13), - .iOS(.v13), - .tvOS(.v13), - .watchOS(.v6), - .macCatalyst(.v13), + .macOS(.v15), + .iOS(.v18), + .watchOS(.v11), + .tvOS(.v18), ], products: [ @@ -65,9 +64,9 @@ let package = Package( .executableTarget( name: "JavaDependencySample", dependencies: [ - .product(name: "JavaKit", package: "swift-java"), - .product(name: "JavaRuntime", package: "swift-java"), - .product(name: "JavaKitFunction", package: "swift-java"), + .product(name: "SwiftJava", package: "swift-java"), + .product(name: "CSwiftJavaJNI", package: "swift-java"), + .product(name: "JavaUtilFunction", package: "swift-java"), "JavaCommonsCSV" ], exclude: ["swift-java.config"], @@ -83,11 +82,11 @@ let package = Package( .target( name: "JavaCommonsCSV", dependencies: [ - .product(name: "JavaKit", package: "swift-java"), - .product(name: "JavaKitFunction", package: "swift-java"), - .product(name: "JavaKitCollection", package: "swift-java"), - .product(name: "JavaKitIO", package: "swift-java"), - .product(name: "JavaKitNetwork", package: "swift-java"), + .product(name: "SwiftJava", package: "swift-java"), + .product(name: "JavaUtilFunction", package: "swift-java"), + .product(name: "JavaUtil", package: "swift-java"), + .product(name: "JavaIO", package: "swift-java"), + .product(name: "JavaNet", package: "swift-java"), ], exclude: ["swift-java.config"], swiftSettings: [ diff --git a/Samples/JavaDependencySampleApp/Sources/JavaCommonsCSV/swift-java.config b/Samples/JavaDependencySampleApp/Sources/JavaCommonsCSV/swift-java.config index 3b685159d..dc38efd9f 100644 --- a/Samples/JavaDependencySampleApp/Sources/JavaCommonsCSV/swift-java.config +++ b/Samples/JavaDependencySampleApp/Sources/JavaCommonsCSV/swift-java.config @@ -1,11 +1,13 @@ { "classes" : { "org.apache.commons.io.FilenameUtils" : "FilenameUtils", - "org.apache.commons.io.IOCase" : "IOCase", "org.apache.commons.csv.CSVFormat" : "CSVFormat", "org.apache.commons.csv.CSVParser" : "CSVParser", "org.apache.commons.csv.CSVRecord" : "CSVRecord" }, + "filterExclude" : [ + "org.apache.commons.csv.CSVFormat$Predefined", + ], "dependencies" : [ "org.apache.commons:commons-csv:1.12.0" ] diff --git a/Samples/JavaDependencySampleApp/Sources/JavaDependencySample/main.swift b/Samples/JavaDependencySampleApp/Sources/JavaDependencySample/main.swift index c75cf5537..13ea6eed3 100644 --- a/Samples/JavaDependencySampleApp/Sources/JavaDependencySample/main.swift +++ b/Samples/JavaDependencySampleApp/Sources/JavaDependencySample/main.swift @@ -12,10 +12,10 @@ // //===----------------------------------------------------------------------===// -import JavaKit -import JavaKitFunction -import JavaKitIO -import JavaKitConfigurationShared +import SwiftJava +import JavaUtilFunction +import JavaIO +import SwiftJavaConfigurationShared import Foundation // Import the commons-csv library wrapper: diff --git a/Samples/JavaDependencySampleApp/Sources/Test/swift-java.config b/Samples/JavaDependencySampleApp/Sources/Test/swift-java.config new file mode 100644 index 000000000..50fd73372 --- /dev/null +++ b/Samples/JavaDependencySampleApp/Sources/Test/swift-java.config @@ -0,0 +1,429 @@ +{ + "classes" : { + "org.apache.commons.codec.BinaryDecoder" : "BinaryDecoder", + "org.apache.commons.codec.BinaryEncoder" : "BinaryEncoder", + "org.apache.commons.codec.CharEncoding" : "CharEncoding", + "org.apache.commons.codec.Charsets" : "Charsets", + "org.apache.commons.codec.CodecPolicy" : "CodecPolicy", + "org.apache.commons.codec.Decoder" : "Decoder", + "org.apache.commons.codec.DecoderException" : "DecoderException", + "org.apache.commons.codec.Encoder" : "Encoder", + "org.apache.commons.codec.EncoderException" : "EncoderException", + "org.apache.commons.codec.Resources" : "Resources", + "org.apache.commons.codec.StringDecoder" : "StringDecoder", + "org.apache.commons.codec.StringEncoder" : "StringEncoder", + "org.apache.commons.codec.StringEncoderComparator" : "StringEncoderComparator", + "org.apache.commons.codec.binary.Base16" : "Base16", + "org.apache.commons.codec.binary.Base16InputStream" : "Base16InputStream", + "org.apache.commons.codec.binary.Base16OutputStream" : "Base16OutputStream", + "org.apache.commons.codec.binary.Base32" : "Base32", + "org.apache.commons.codec.binary.Base32$Builder" : "Base32.Builder", + "org.apache.commons.codec.binary.Base32InputStream" : "Base32InputStream", + "org.apache.commons.codec.binary.Base32OutputStream" : "Base32OutputStream", + "org.apache.commons.codec.binary.Base64" : "Base64", + "org.apache.commons.codec.binary.Base64$Builder" : "Base64.Builder", + "org.apache.commons.codec.binary.Base64InputStream" : "Base64InputStream", + "org.apache.commons.codec.binary.Base64OutputStream" : "Base64OutputStream", + "org.apache.commons.codec.binary.BaseNCodec" : "BaseNCodec", + "org.apache.commons.codec.binary.BaseNCodec$AbstractBuilder" : "BaseNCodec.AbstractBuilder", + "org.apache.commons.codec.binary.BaseNCodec$Context" : "BaseNCodec.Context", + "org.apache.commons.codec.binary.BaseNCodecInputStream" : "BaseNCodecInputStream", + "org.apache.commons.codec.binary.BaseNCodecOutputStream" : "BaseNCodecOutputStream", + "org.apache.commons.codec.binary.BinaryCodec" : "BinaryCodec", + "org.apache.commons.codec.binary.CharSequenceUtils" : "CharSequenceUtils", + "org.apache.commons.codec.binary.Hex" : "Hex", + "org.apache.commons.codec.binary.StringUtils" : "StringUtils", + "org.apache.commons.codec.cli.Digest" : "Digest", + "org.apache.commons.codec.digest.B64" : "B64", + "org.apache.commons.codec.digest.Blake3" : "Blake3", + "org.apache.commons.codec.digest.Blake3$ChunkState" : "Blake3.ChunkState", + "org.apache.commons.codec.digest.Blake3$EngineState" : "Blake3.EngineState", + "org.apache.commons.codec.digest.Blake3$Output" : "Blake3.Output", + "org.apache.commons.codec.digest.Crypt" : "Crypt", + "org.apache.commons.codec.digest.DigestUtils" : "DigestUtils", + "org.apache.commons.codec.digest.HmacAlgorithms" : "HmacAlgorithms", + "org.apache.commons.codec.digest.HmacUtils" : "HmacUtils", + "org.apache.commons.codec.digest.Md5Crypt" : "Md5Crypt", + "org.apache.commons.codec.digest.MessageDigestAlgorithms" : "MessageDigestAlgorithms", + "org.apache.commons.codec.digest.MurmurHash2" : "MurmurHash2", + "org.apache.commons.codec.digest.MurmurHash3" : "MurmurHash3", + "org.apache.commons.codec.digest.MurmurHash3$IncrementalHash32" : "MurmurHash3.IncrementalHash32", + "org.apache.commons.codec.digest.MurmurHash3$IncrementalHash32x86" : "MurmurHash3.IncrementalHash32x86", + "org.apache.commons.codec.digest.PureJavaCrc32" : "PureJavaCrc32", + "org.apache.commons.codec.digest.PureJavaCrc32C" : "PureJavaCrc32C", + "org.apache.commons.codec.digest.Sha2Crypt" : "Sha2Crypt", + "org.apache.commons.codec.digest.UnixCrypt" : "UnixCrypt", + "org.apache.commons.codec.digest.XXHash32" : "XXHash32", + "org.apache.commons.codec.language.AbstractCaverphone" : "AbstractCaverphone", + "org.apache.commons.codec.language.Caverphone" : "Caverphone", + "org.apache.commons.codec.language.Caverphone1" : "Caverphone1", + "org.apache.commons.codec.language.Caverphone2" : "Caverphone2", + "org.apache.commons.codec.language.ColognePhonetic" : "ColognePhonetic", + "org.apache.commons.codec.language.ColognePhonetic$CologneBuffer" : "ColognePhonetic.CologneBuffer", + "org.apache.commons.codec.language.ColognePhonetic$CologneInputBuffer" : "ColognePhonetic.CologneInputBuffer", + "org.apache.commons.codec.language.ColognePhonetic$CologneOutputBuffer" : "ColognePhonetic.CologneOutputBuffer", + "org.apache.commons.codec.language.DaitchMokotoffSoundex" : "DaitchMokotoffSoundex", + "org.apache.commons.codec.language.DaitchMokotoffSoundex$Branch" : "DaitchMokotoffSoundex.Branch", + "org.apache.commons.codec.language.DaitchMokotoffSoundex$Rule" : "DaitchMokotoffSoundex.Rule", + "org.apache.commons.codec.language.DoubleMetaphone" : "DoubleMetaphone", + "org.apache.commons.codec.language.DoubleMetaphone$DoubleMetaphoneResult" : "DoubleMetaphone.DoubleMetaphoneResult", + "org.apache.commons.codec.language.MatchRatingApproachEncoder" : "MatchRatingApproachEncoder", + "org.apache.commons.codec.language.Metaphone" : "Metaphone", + "org.apache.commons.codec.language.Nysiis" : "Nysiis", + "org.apache.commons.codec.language.RefinedSoundex" : "RefinedSoundex", + "org.apache.commons.codec.language.Soundex" : "Soundex", + "org.apache.commons.codec.language.SoundexUtils" : "SoundexUtils", + "org.apache.commons.codec.language.bm.BeiderMorseEncoder" : "BeiderMorseEncoder", + "org.apache.commons.codec.language.bm.Lang" : "Lang", + "org.apache.commons.codec.language.bm.Lang$LangRule" : "Lang.LangRule", + "org.apache.commons.codec.language.bm.Languages" : "Languages", + "org.apache.commons.codec.language.bm.Languages$LanguageSet" : "Languages.LanguageSet", + "org.apache.commons.codec.language.bm.Languages$SomeLanguages" : "Languages.SomeLanguages", + "org.apache.commons.codec.language.bm.NameType" : "NameType", + "org.apache.commons.codec.language.bm.PhoneticEngine" : "PhoneticEngine", + "org.apache.commons.codec.language.bm.PhoneticEngine$PhonemeBuilder" : "PhoneticEngine.PhonemeBuilder", + "org.apache.commons.codec.language.bm.PhoneticEngine$RulesApplication" : "PhoneticEngine.RulesApplication", + "org.apache.commons.codec.language.bm.ResourceConstants" : "ResourceConstants", + "org.apache.commons.codec.language.bm.Rule" : "Rule", + "org.apache.commons.codec.language.bm.Rule$Phoneme" : "Rule.Phoneme", + "org.apache.commons.codec.language.bm.Rule$PhonemeExpr" : "Rule.PhonemeExpr", + "org.apache.commons.codec.language.bm.Rule$PhonemeList" : "Rule.PhonemeList", + "org.apache.commons.codec.language.bm.Rule$RPattern" : "Rule.RPattern", + "org.apache.commons.codec.language.bm.RuleType" : "RuleType", + "org.apache.commons.codec.net.BCodec" : "BCodec", + "org.apache.commons.codec.net.PercentCodec" : "PercentCodec", + "org.apache.commons.codec.net.QCodec" : "QCodec", + "org.apache.commons.codec.net.QuotedPrintableCodec" : "QuotedPrintableCodec", + "org.apache.commons.codec.net.RFC1522Codec" : "RFC1522Codec", + "org.apache.commons.codec.net.URLCodec" : "URLCodec", + "org.apache.commons.codec.net.Utils" : "Utils", + "org.apache.commons.csv.CSVException" : "CSVException", + "org.apache.commons.csv.CSVFormat" : "CSVFormat", + "org.apache.commons.csv.CSVFormat$Builder" : "CSVFormat.Builder", + "org.apache.commons.csv.CSVFormat$Predefined" : "CSVFormat.Predefined", + "org.apache.commons.csv.CSVParser" : "CSVParser", + "org.apache.commons.csv.CSVParser$CSVRecordIterator" : "CSVParser.CSVRecordIterator", + "org.apache.commons.csv.CSVParser$Headers" : "CSVParser.Headers", + "org.apache.commons.csv.CSVPrinter" : "CSVPrinter", + "org.apache.commons.csv.CSVRecord" : "CSVRecord", + "org.apache.commons.csv.Constants" : "Constants", + "org.apache.commons.csv.DuplicateHeaderMode" : "DuplicateHeaderMode", + "org.apache.commons.csv.ExtendedBufferedReader" : "ExtendedBufferedReader", + "org.apache.commons.csv.Lexer" : "Lexer", + "org.apache.commons.csv.QuoteMode" : "QuoteMode", + "org.apache.commons.csv.Token" : "Token", + "org.apache.commons.csv.Token$Type" : "Token.Type", + "org.apache.commons.io.ByteOrderMark" : "ByteOrderMark", + "org.apache.commons.io.ByteOrderParser" : "ByteOrderParser", + "org.apache.commons.io.Charsets" : "Charsets", + "org.apache.commons.io.CloseableURLConnection" : "CloseableURLConnection", + "org.apache.commons.io.CopyUtils" : "CopyUtils", + "org.apache.commons.io.DirectoryWalker" : "DirectoryWalker", + "org.apache.commons.io.DirectoryWalker$CancelException" : "DirectoryWalker.CancelException", + "org.apache.commons.io.EndianUtils" : "EndianUtils", + "org.apache.commons.io.FileCleaner" : "FileCleaner", + "org.apache.commons.io.FileCleaningTracker" : "FileCleaningTracker", + "org.apache.commons.io.FileCleaningTracker$Reaper" : "FileCleaningTracker.Reaper", + "org.apache.commons.io.FileCleaningTracker$Tracker" : "FileCleaningTracker.Tracker", + "org.apache.commons.io.FileDeleteStrategy" : "FileDeleteStrategy", + "org.apache.commons.io.FileDeleteStrategy$ForceFileDeleteStrategy" : "FileDeleteStrategy.ForceFileDeleteStrategy", + "org.apache.commons.io.FileExistsException" : "FileExistsException", + "org.apache.commons.io.FileSystem" : "FileSystem", + "org.apache.commons.io.FileSystemUtils" : "FileSystemUtils", + "org.apache.commons.io.FileUtils" : "FileUtils", + "org.apache.commons.io.FilenameUtils" : "FilenameUtils", + "org.apache.commons.io.HexDump" : "HexDump", + "org.apache.commons.io.IO" : "IO", + "org.apache.commons.io.IOCase" : "IOCase", + "org.apache.commons.io.IOExceptionList" : "IOExceptionList", + "org.apache.commons.io.IOExceptionWithCause" : "IOExceptionWithCause", + "org.apache.commons.io.IOIndexedException" : "IOIndexedException", + "org.apache.commons.io.IOUtils" : "IOUtils", + "org.apache.commons.io.LineIterator" : "LineIterator", + "org.apache.commons.io.RandomAccessFileMode" : "RandomAccessFileMode", + "org.apache.commons.io.RandomAccessFiles" : "RandomAccessFiles", + "org.apache.commons.io.StandardLineSeparator" : "StandardLineSeparator", + "org.apache.commons.io.StreamIterator" : "StreamIterator", + "org.apache.commons.io.TaggedIOException" : "TaggedIOException", + "org.apache.commons.io.ThreadMonitor" : "ThreadMonitor", + "org.apache.commons.io.ThreadUtils" : "ThreadUtils", + "org.apache.commons.io.UncheckedIOExceptions" : "UncheckedIOExceptions", + "org.apache.commons.io.build.AbstractOrigin" : "AbstractOrigin", + "org.apache.commons.io.build.AbstractOrigin$ByteArrayOrigin" : "AbstractOrigin.ByteArrayOrigin", + "org.apache.commons.io.build.AbstractOrigin$CharSequenceOrigin" : "AbstractOrigin.CharSequenceOrigin", + "org.apache.commons.io.build.AbstractOrigin$FileOrigin" : "AbstractOrigin.FileOrigin", + "org.apache.commons.io.build.AbstractOrigin$InputStreamOrigin" : "AbstractOrigin.InputStreamOrigin", + "org.apache.commons.io.build.AbstractOrigin$OutputStreamOrigin" : "AbstractOrigin.OutputStreamOrigin", + "org.apache.commons.io.build.AbstractOrigin$PathOrigin" : "AbstractOrigin.PathOrigin", + "org.apache.commons.io.build.AbstractOrigin$ReaderOrigin" : "AbstractOrigin.ReaderOrigin", + "org.apache.commons.io.build.AbstractOrigin$URIOrigin" : "AbstractOrigin.URIOrigin", + "org.apache.commons.io.build.AbstractOrigin$WriterOrigin" : "AbstractOrigin.WriterOrigin", + "org.apache.commons.io.build.AbstractOriginSupplier" : "AbstractOriginSupplier", + "org.apache.commons.io.build.AbstractStreamBuilder" : "AbstractStreamBuilder", + "org.apache.commons.io.build.AbstractSupplier" : "AbstractSupplier", + "org.apache.commons.io.channels.FileChannels" : "FileChannels", + "org.apache.commons.io.charset.CharsetDecoders" : "CharsetDecoders", + "org.apache.commons.io.charset.CharsetEncoders" : "CharsetEncoders", + "org.apache.commons.io.comparator.AbstractFileComparator" : "AbstractFileComparator", + "org.apache.commons.io.comparator.CompositeFileComparator" : "CompositeFileComparator", + "org.apache.commons.io.comparator.DefaultFileComparator" : "DefaultFileComparator", + "org.apache.commons.io.comparator.DirectoryFileComparator" : "DirectoryFileComparator", + "org.apache.commons.io.comparator.ExtensionFileComparator" : "ExtensionFileComparator", + "org.apache.commons.io.comparator.LastModifiedFileComparator" : "LastModifiedFileComparator", + "org.apache.commons.io.comparator.NameFileComparator" : "NameFileComparator", + "org.apache.commons.io.comparator.PathFileComparator" : "PathFileComparator", + "org.apache.commons.io.comparator.ReverseFileComparator" : "ReverseFileComparator", + "org.apache.commons.io.comparator.SizeFileComparator" : "SizeFileComparator", + "org.apache.commons.io.file.AccumulatorPathVisitor" : "AccumulatorPathVisitor", + "org.apache.commons.io.file.CleaningPathVisitor" : "CleaningPathVisitor", + "org.apache.commons.io.file.CopyDirectoryVisitor" : "CopyDirectoryVisitor", + "org.apache.commons.io.file.Counters" : "Counters", + "org.apache.commons.io.file.Counters$AbstractPathCounters" : "Counters.AbstractPathCounters", + "org.apache.commons.io.file.Counters$BigIntegerCounter" : "Counters.BigIntegerCounter", + "org.apache.commons.io.file.Counters$BigIntegerPathCounters" : "Counters.BigIntegerPathCounters", + "org.apache.commons.io.file.Counters$Counter" : "Counters.Counter", + "org.apache.commons.io.file.Counters$LongCounter" : "Counters.LongCounter", + "org.apache.commons.io.file.Counters$LongPathCounters" : "Counters.LongPathCounters", + "org.apache.commons.io.file.Counters$NoopCounter" : "Counters.NoopCounter", + "org.apache.commons.io.file.Counters$NoopPathCounters" : "Counters.NoopPathCounters", + "org.apache.commons.io.file.Counters$PathCounters" : "Counters.PathCounters", + "org.apache.commons.io.file.CountingPathVisitor" : "CountingPathVisitor", + "org.apache.commons.io.file.DeleteOption" : "DeleteOption", + "org.apache.commons.io.file.DeletingPathVisitor" : "DeletingPathVisitor", + "org.apache.commons.io.file.DirectoryStreamFilter" : "DirectoryStreamFilter", + "org.apache.commons.io.file.FilesUncheck" : "FilesUncheck", + "org.apache.commons.io.file.NoopPathVisitor" : "NoopPathVisitor", + "org.apache.commons.io.file.PathFilter" : "PathFilter", + "org.apache.commons.io.file.PathUtils" : "PathUtils", + "org.apache.commons.io.file.PathUtils$RelativeSortedPaths" : "PathUtils.RelativeSortedPaths", + "org.apache.commons.io.file.PathVisitor" : "PathVisitor", + "org.apache.commons.io.file.SimplePathVisitor" : "SimplePathVisitor", + "org.apache.commons.io.file.StandardDeleteOption" : "StandardDeleteOption", + "org.apache.commons.io.file.attribute.FileTimes" : "FileTimes", + "org.apache.commons.io.file.spi.FileSystemProviders" : "FileSystemProviders", + "org.apache.commons.io.filefilter.AbstractFileFilter" : "AbstractFileFilter", + "org.apache.commons.io.filefilter.AgeFileFilter" : "AgeFileFilter", + "org.apache.commons.io.filefilter.AndFileFilter" : "AndFileFilter", + "org.apache.commons.io.filefilter.CanExecuteFileFilter" : "CanExecuteFileFilter", + "org.apache.commons.io.filefilter.CanReadFileFilter" : "CanReadFileFilter", + "org.apache.commons.io.filefilter.CanWriteFileFilter" : "CanWriteFileFilter", + "org.apache.commons.io.filefilter.ConditionalFileFilter" : "ConditionalFileFilter", + "org.apache.commons.io.filefilter.DelegateFileFilter" : "DelegateFileFilter", + "org.apache.commons.io.filefilter.DirectoryFileFilter" : "DirectoryFileFilter", + "org.apache.commons.io.filefilter.EmptyFileFilter" : "EmptyFileFilter", + "org.apache.commons.io.filefilter.FalseFileFilter" : "FalseFileFilter", + "org.apache.commons.io.filefilter.FileEqualsFileFilter" : "FileEqualsFileFilter", + "org.apache.commons.io.filefilter.FileFileFilter" : "FileFileFilter", + "org.apache.commons.io.filefilter.FileFilterUtils" : "FileFilterUtils", + "org.apache.commons.io.filefilter.HiddenFileFilter" : "HiddenFileFilter", + "org.apache.commons.io.filefilter.IOFileFilter" : "IOFileFilter", + "org.apache.commons.io.filefilter.MagicNumberFileFilter" : "MagicNumberFileFilter", + "org.apache.commons.io.filefilter.NameFileFilter" : "NameFileFilter", + "org.apache.commons.io.filefilter.NotFileFilter" : "NotFileFilter", + "org.apache.commons.io.filefilter.OrFileFilter" : "OrFileFilter", + "org.apache.commons.io.filefilter.PathEqualsFileFilter" : "PathEqualsFileFilter", + "org.apache.commons.io.filefilter.PathMatcherFileFilter" : "PathMatcherFileFilter", + "org.apache.commons.io.filefilter.PathVisitorFileFilter" : "PathVisitorFileFilter", + "org.apache.commons.io.filefilter.PrefixFileFilter" : "PrefixFileFilter", + "org.apache.commons.io.filefilter.RegexFileFilter" : "RegexFileFilter", + "org.apache.commons.io.filefilter.SizeFileFilter" : "SizeFileFilter", + "org.apache.commons.io.filefilter.SuffixFileFilter" : "SuffixFileFilter", + "org.apache.commons.io.filefilter.SymbolicLinkFileFilter" : "SymbolicLinkFileFilter", + "org.apache.commons.io.filefilter.TrueFileFilter" : "TrueFileFilter", + "org.apache.commons.io.filefilter.WildcardFileFilter" : "WildcardFileFilter", + "org.apache.commons.io.filefilter.WildcardFileFilter$Builder" : "WildcardFileFilter.Builder", + "org.apache.commons.io.filefilter.WildcardFilter" : "WildcardFilter", + "org.apache.commons.io.function.Constants" : "Constants", + "org.apache.commons.io.function.Erase" : "Erase", + "org.apache.commons.io.function.IOBaseStream" : "IOBaseStream", + "org.apache.commons.io.function.IOBaseStreamAdapter" : "IOBaseStreamAdapter", + "org.apache.commons.io.function.IOBiConsumer" : "IOBiConsumer", + "org.apache.commons.io.function.IOBiFunction" : "IOBiFunction", + "org.apache.commons.io.function.IOBinaryOperator" : "IOBinaryOperator", + "org.apache.commons.io.function.IOComparator" : "IOComparator", + "org.apache.commons.io.function.IOConsumer" : "IOConsumer", + "org.apache.commons.io.function.IOFunction" : "IOFunction", + "org.apache.commons.io.function.IOIntSupplier" : "IOIntSupplier", + "org.apache.commons.io.function.IOIterator" : "IOIterator", + "org.apache.commons.io.function.IOIteratorAdapter" : "IOIteratorAdapter", + "org.apache.commons.io.function.IOLongSupplier" : "IOLongSupplier", + "org.apache.commons.io.function.IOPredicate" : "IOPredicate", + "org.apache.commons.io.function.IOQuadFunction" : "IOQuadFunction", + "org.apache.commons.io.function.IORunnable" : "IORunnable", + "org.apache.commons.io.function.IOSpliterator" : "IOSpliterator", + "org.apache.commons.io.function.IOSpliteratorAdapter" : "IOSpliteratorAdapter", + "org.apache.commons.io.function.IOStream" : "IOStream", + "org.apache.commons.io.function.IOStreamAdapter" : "IOStreamAdapter", + "org.apache.commons.io.function.IOStreams" : "IOStreams", + "org.apache.commons.io.function.IOSupplier" : "IOSupplier", + "org.apache.commons.io.function.IOTriConsumer" : "IOTriConsumer", + "org.apache.commons.io.function.IOTriFunction" : "IOTriFunction", + "org.apache.commons.io.function.IOUnaryOperator" : "IOUnaryOperator", + "org.apache.commons.io.function.Uncheck" : "Uncheck", + "org.apache.commons.io.function.UncheckedIOBaseStream" : "UncheckedIOBaseStream", + "org.apache.commons.io.function.UncheckedIOIterator" : "UncheckedIOIterator", + "org.apache.commons.io.function.UncheckedIOSpliterator" : "UncheckedIOSpliterator", + "org.apache.commons.io.input.AbstractCharacterFilterReader" : "AbstractCharacterFilterReader", + "org.apache.commons.io.input.AbstractInputStream" : "AbstractInputStream", + "org.apache.commons.io.input.AutoCloseInputStream" : "AutoCloseInputStream", + "org.apache.commons.io.input.AutoCloseInputStream$Builder" : "AutoCloseInputStream.Builder", + "org.apache.commons.io.input.BOMInputStream" : "BOMInputStream", + "org.apache.commons.io.input.BOMInputStream$Builder" : "BOMInputStream.Builder", + "org.apache.commons.io.input.BoundedInputStream" : "BoundedInputStream", + "org.apache.commons.io.input.BoundedInputStream$AbstractBuilder" : "BoundedInputStream.AbstractBuilder", + "org.apache.commons.io.input.BoundedInputStream$Builder" : "BoundedInputStream.Builder", + "org.apache.commons.io.input.BoundedReader" : "BoundedReader", + "org.apache.commons.io.input.BrokenInputStream" : "BrokenInputStream", + "org.apache.commons.io.input.BrokenReader" : "BrokenReader", + "org.apache.commons.io.input.BufferedFileChannelInputStream" : "BufferedFileChannelInputStream", + "org.apache.commons.io.input.BufferedFileChannelInputStream$Builder" : "BufferedFileChannelInputStream.Builder", + "org.apache.commons.io.input.ByteBufferCleaner" : "ByteBufferCleaner", + "org.apache.commons.io.input.ByteBufferCleaner$Cleaner" : "ByteBufferCleaner.Cleaner", + "org.apache.commons.io.input.ByteBufferCleaner$Java8Cleaner" : "ByteBufferCleaner.Java8Cleaner", + "org.apache.commons.io.input.ByteBufferCleaner$Java9Cleaner" : "ByteBufferCleaner.Java9Cleaner", + "org.apache.commons.io.input.CharSequenceInputStream" : "CharSequenceInputStream", + "org.apache.commons.io.input.CharSequenceInputStream$Builder" : "CharSequenceInputStream.Builder", + "org.apache.commons.io.input.CharSequenceReader" : "CharSequenceReader", + "org.apache.commons.io.input.CharacterFilterReader" : "CharacterFilterReader", + "org.apache.commons.io.input.CharacterSetFilterReader" : "CharacterSetFilterReader", + "org.apache.commons.io.input.ChecksumInputStream" : "ChecksumInputStream", + "org.apache.commons.io.input.ChecksumInputStream$Builder" : "ChecksumInputStream.Builder", + "org.apache.commons.io.input.CircularInputStream" : "CircularInputStream", + "org.apache.commons.io.input.ClassLoaderObjectInputStream" : "ClassLoaderObjectInputStream", + "org.apache.commons.io.input.CloseShieldInputStream" : "CloseShieldInputStream", + "org.apache.commons.io.input.CloseShieldReader" : "CloseShieldReader", + "org.apache.commons.io.input.ClosedInputStream" : "ClosedInputStream", + "org.apache.commons.io.input.ClosedReader" : "ClosedReader", + "org.apache.commons.io.input.CountingInputStream" : "CountingInputStream", + "org.apache.commons.io.input.DemuxInputStream" : "DemuxInputStream", + "org.apache.commons.io.input.InfiniteCircularInputStream" : "InfiniteCircularInputStream", + "org.apache.commons.io.input.Input" : "Input", + "org.apache.commons.io.input.MarkShieldInputStream" : "MarkShieldInputStream", + "org.apache.commons.io.input.MemoryMappedFileInputStream" : "MemoryMappedFileInputStream", + "org.apache.commons.io.input.MemoryMappedFileInputStream$Builder" : "MemoryMappedFileInputStream.Builder", + "org.apache.commons.io.input.MessageDigestCalculatingInputStream" : "MessageDigestCalculatingInputStream", + "org.apache.commons.io.input.MessageDigestCalculatingInputStream$Builder" : "MessageDigestCalculatingInputStream.Builder", + "org.apache.commons.io.input.MessageDigestCalculatingInputStream$MessageDigestMaintainingObserver" : "MessageDigestCalculatingInputStream.MessageDigestMaintainingObserver", + "org.apache.commons.io.input.MessageDigestInputStream" : "MessageDigestInputStream", + "org.apache.commons.io.input.MessageDigestInputStream$Builder" : "MessageDigestInputStream.Builder", + "org.apache.commons.io.input.MessageDigestInputStream$MessageDigestMaintainingObserver" : "MessageDigestInputStream.MessageDigestMaintainingObserver", + "org.apache.commons.io.input.NullInputStream" : "NullInputStream", + "org.apache.commons.io.input.NullReader" : "NullReader", + "org.apache.commons.io.input.ObservableInputStream" : "ObservableInputStream", + "org.apache.commons.io.input.ObservableInputStream$Observer" : "ObservableInputStream.Observer", + "org.apache.commons.io.input.ProxyInputStream" : "ProxyInputStream", + "org.apache.commons.io.input.ProxyReader" : "ProxyReader", + "org.apache.commons.io.input.QueueInputStream" : "QueueInputStream", + "org.apache.commons.io.input.QueueInputStream$Builder" : "QueueInputStream.Builder", + "org.apache.commons.io.input.RandomAccessFileInputStream" : "RandomAccessFileInputStream", + "org.apache.commons.io.input.RandomAccessFileInputStream$Builder" : "RandomAccessFileInputStream.Builder", + "org.apache.commons.io.input.ReadAheadInputStream" : "ReadAheadInputStream", + "org.apache.commons.io.input.ReadAheadInputStream$Builder" : "ReadAheadInputStream.Builder", + "org.apache.commons.io.input.ReaderInputStream" : "ReaderInputStream", + "org.apache.commons.io.input.ReaderInputStream$Builder" : "ReaderInputStream.Builder", + "org.apache.commons.io.input.ReversedLinesFileReader" : "ReversedLinesFileReader", + "org.apache.commons.io.input.ReversedLinesFileReader$Builder" : "ReversedLinesFileReader.Builder", + "org.apache.commons.io.input.ReversedLinesFileReader$FilePart" : "ReversedLinesFileReader.FilePart", + "org.apache.commons.io.input.SequenceReader" : "SequenceReader", + "org.apache.commons.io.input.SwappedDataInputStream" : "SwappedDataInputStream", + "org.apache.commons.io.input.TaggedInputStream" : "TaggedInputStream", + "org.apache.commons.io.input.TaggedReader" : "TaggedReader", + "org.apache.commons.io.input.Tailer" : "Tailer", + "org.apache.commons.io.input.Tailer$Builder" : "Tailer.Builder", + "org.apache.commons.io.input.Tailer$RandomAccessFileBridge" : "Tailer.RandomAccessFileBridge", + "org.apache.commons.io.input.Tailer$RandomAccessResourceBridge" : "Tailer.RandomAccessResourceBridge", + "org.apache.commons.io.input.Tailer$Tailable" : "Tailer.Tailable", + "org.apache.commons.io.input.Tailer$TailablePath" : "Tailer.TailablePath", + "org.apache.commons.io.input.TailerListener" : "TailerListener", + "org.apache.commons.io.input.TailerListenerAdapter" : "TailerListenerAdapter", + "org.apache.commons.io.input.TeeInputStream" : "TeeInputStream", + "org.apache.commons.io.input.TeeReader" : "TeeReader", + "org.apache.commons.io.input.ThrottledInputStream" : "ThrottledInputStream", + "org.apache.commons.io.input.ThrottledInputStream$Builder" : "ThrottledInputStream.Builder", + "org.apache.commons.io.input.TimestampedObserver" : "TimestampedObserver", + "org.apache.commons.io.input.UncheckedBufferedReader" : "UncheckedBufferedReader", + "org.apache.commons.io.input.UncheckedBufferedReader$Builder" : "UncheckedBufferedReader.Builder", + "org.apache.commons.io.input.UncheckedFilterInputStream" : "UncheckedFilterInputStream", + "org.apache.commons.io.input.UncheckedFilterInputStream$Builder" : "UncheckedFilterInputStream.Builder", + "org.apache.commons.io.input.UncheckedFilterReader" : "UncheckedFilterReader", + "org.apache.commons.io.input.UncheckedFilterReader$Builder" : "UncheckedFilterReader.Builder", + "org.apache.commons.io.input.UnixLineEndingInputStream" : "UnixLineEndingInputStream", + "org.apache.commons.io.input.UnsupportedOperationExceptions" : "UnsupportedOperationExceptions", + "org.apache.commons.io.input.UnsynchronizedBufferedInputStream" : "UnsynchronizedBufferedInputStream", + "org.apache.commons.io.input.UnsynchronizedBufferedInputStream$Builder" : "UnsynchronizedBufferedInputStream.Builder", + "org.apache.commons.io.input.UnsynchronizedBufferedReader" : "UnsynchronizedBufferedReader", + "org.apache.commons.io.input.UnsynchronizedByteArrayInputStream" : "UnsynchronizedByteArrayInputStream", + "org.apache.commons.io.input.UnsynchronizedByteArrayInputStream$Builder" : "UnsynchronizedByteArrayInputStream.Builder", + "org.apache.commons.io.input.UnsynchronizedFilterInputStream" : "UnsynchronizedFilterInputStream", + "org.apache.commons.io.input.UnsynchronizedFilterInputStream$Builder" : "UnsynchronizedFilterInputStream.Builder", + "org.apache.commons.io.input.UnsynchronizedReader" : "UnsynchronizedReader", + "org.apache.commons.io.input.WindowsLineEndingInputStream" : "WindowsLineEndingInputStream", + "org.apache.commons.io.input.XmlStreamReader" : "XmlStreamReader", + "org.apache.commons.io.input.XmlStreamReader$Builder" : "XmlStreamReader.Builder", + "org.apache.commons.io.input.XmlStreamReaderException" : "XmlStreamReaderException", + "org.apache.commons.io.input.buffer.CircularBufferInputStream" : "CircularBufferInputStream", + "org.apache.commons.io.input.buffer.CircularByteBuffer" : "CircularByteBuffer", + "org.apache.commons.io.input.buffer.PeekableInputStream" : "PeekableInputStream", + "org.apache.commons.io.monitor.FileAlterationListener" : "FileAlterationListener", + "org.apache.commons.io.monitor.FileAlterationListenerAdaptor" : "FileAlterationListenerAdaptor", + "org.apache.commons.io.monitor.FileAlterationMonitor" : "FileAlterationMonitor", + "org.apache.commons.io.monitor.FileAlterationObserver" : "FileAlterationObserver", + "org.apache.commons.io.monitor.FileEntry" : "FileEntry", + "org.apache.commons.io.monitor.SerializableFileTime" : "SerializableFileTime", + "org.apache.commons.io.output.AbstractByteArrayOutputStream" : "AbstractByteArrayOutputStream", + "org.apache.commons.io.output.AbstractByteArrayOutputStream$InputStreamConstructor" : "AbstractByteArrayOutputStream.InputStreamConstructor", + "org.apache.commons.io.output.AppendableOutputStream" : "AppendableOutputStream", + "org.apache.commons.io.output.AppendableWriter" : "AppendableWriter", + "org.apache.commons.io.output.BrokenOutputStream" : "BrokenOutputStream", + "org.apache.commons.io.output.BrokenWriter" : "BrokenWriter", + "org.apache.commons.io.output.ByteArrayOutputStream" : "ByteArrayOutputStream", + "org.apache.commons.io.output.ChunkedOutputStream" : "ChunkedOutputStream", + "org.apache.commons.io.output.ChunkedOutputStream$Builder" : "ChunkedOutputStream.Builder", + "org.apache.commons.io.output.ChunkedWriter" : "ChunkedWriter", + "org.apache.commons.io.output.CloseShieldOutputStream" : "CloseShieldOutputStream", + "org.apache.commons.io.output.CloseShieldWriter" : "CloseShieldWriter", + "org.apache.commons.io.output.ClosedOutputStream" : "ClosedOutputStream", + "org.apache.commons.io.output.ClosedWriter" : "ClosedWriter", + "org.apache.commons.io.output.CountingOutputStream" : "CountingOutputStream", + "org.apache.commons.io.output.DeferredFileOutputStream" : "DeferredFileOutputStream", + "org.apache.commons.io.output.DeferredFileOutputStream$Builder" : "DeferredFileOutputStream.Builder", + "org.apache.commons.io.output.DemuxOutputStream" : "DemuxOutputStream", + "org.apache.commons.io.output.FileWriterWithEncoding" : "FileWriterWithEncoding", + "org.apache.commons.io.output.FileWriterWithEncoding$Builder" : "FileWriterWithEncoding.Builder", + "org.apache.commons.io.output.FilterCollectionWriter" : "FilterCollectionWriter", + "org.apache.commons.io.output.LockableFileWriter" : "LockableFileWriter", + "org.apache.commons.io.output.LockableFileWriter$Builder" : "LockableFileWriter.Builder", + "org.apache.commons.io.output.NullAppendable" : "NullAppendable", + "org.apache.commons.io.output.NullOutputStream" : "NullOutputStream", + "org.apache.commons.io.output.NullPrintStream" : "NullPrintStream", + "org.apache.commons.io.output.NullWriter" : "NullWriter", + "org.apache.commons.io.output.ProxyCollectionWriter" : "ProxyCollectionWriter", + "org.apache.commons.io.output.ProxyOutputStream" : "ProxyOutputStream", + "org.apache.commons.io.output.ProxyWriter" : "ProxyWriter", + "org.apache.commons.io.output.QueueOutputStream" : "QueueOutputStream", + "org.apache.commons.io.output.StringBuilderWriter" : "StringBuilderWriter", + "org.apache.commons.io.output.TaggedOutputStream" : "TaggedOutputStream", + "org.apache.commons.io.output.TaggedWriter" : "TaggedWriter", + "org.apache.commons.io.output.TeeOutputStream" : "TeeOutputStream", + "org.apache.commons.io.output.TeeWriter" : "TeeWriter", + "org.apache.commons.io.output.ThresholdingOutputStream" : "ThresholdingOutputStream", + "org.apache.commons.io.output.UncheckedAppendable" : "UncheckedAppendable", + "org.apache.commons.io.output.UncheckedAppendableImpl" : "UncheckedAppendableImpl", + "org.apache.commons.io.output.UncheckedFilterOutputStream" : "UncheckedFilterOutputStream", + "org.apache.commons.io.output.UncheckedFilterOutputStream$Builder" : "UncheckedFilterOutputStream.Builder", + "org.apache.commons.io.output.UncheckedFilterWriter" : "UncheckedFilterWriter", + "org.apache.commons.io.output.UncheckedFilterWriter$Builder" : "UncheckedFilterWriter.Builder", + "org.apache.commons.io.output.UnsynchronizedByteArrayOutputStream" : "UnsynchronizedByteArrayOutputStream", + "org.apache.commons.io.output.UnsynchronizedByteArrayOutputStream$Builder" : "UnsynchronizedByteArrayOutputStream.Builder", + "org.apache.commons.io.output.WriterOutputStream" : "WriterOutputStream", + "org.apache.commons.io.output.WriterOutputStream$Builder" : "WriterOutputStream.Builder", + "org.apache.commons.io.output.XmlStreamWriter" : "XmlStreamWriter", + "org.apache.commons.io.output.XmlStreamWriter$Builder" : "XmlStreamWriter.Builder", + "org.apache.commons.io.serialization.ClassNameMatcher" : "ClassNameMatcher", + "org.apache.commons.io.serialization.FullClassNameMatcher" : "FullClassNameMatcher", + "org.apache.commons.io.serialization.RegexpClassNameMatcher" : "RegexpClassNameMatcher", + "org.apache.commons.io.serialization.ValidatingObjectInputStream" : "ValidatingObjectInputStream", + "org.apache.commons.io.serialization.WildcardClassNameMatcher" : "WildcardClassNameMatcher" + }, + "classpath" : "\/Users\/ktoso\/code\/swift-java\/Samples\/JavaDependencySampleApp\/.build\/swift-java-dependencies-C358EF3D-93BF-4D44-9523-80865DF0B59D\/build\/classes\/java\/main:\/Users\/ktoso\/code\/swift-java\/Samples\/JavaDependencySampleApp\/.build\/swift-java-dependencies-C358EF3D-93BF-4D44-9523-80865DF0B59D\/build\/resources\/main:\/Users\/ktoso\/.gradle\/caches\/modules-2\/files-2.1\/org.apache.commons\/commons-csv\/1.12.0\/c77e053d7189bc0857f8d323ab61cb949965fbd1\/commons-csv-1.12.0.jar:\/Users\/ktoso\/.gradle\/caches\/modules-2\/files-2.1\/commons-io\/commons-io\/2.17.0\/ddcc8433eb019fb48fe25207c0278143f3e1d7e2\/commons-io-2.17.0.jar:\/Users\/ktoso\/.gradle\/caches\/modules-2\/files-2.1\/commons-codec\/commons-codec\/1.17.1\/973638b7149d333563584137ebf13a691bb60579\/commons-codec-1.17.1.jar:\/Users\/ktoso\/code\/swift-java\/Samples\/JavaDependencySampleApp\/.build\/swift-java-dependencies-C358EF3D-93BF-4D44-9523-80865DF0B59D\/build\/classes\/java\/main:\/Users\/ktoso\/code\/swift-java\/Samples\/JavaDependencySampleApp\/.build\/swift-java-dependencies-C358EF3D-93BF-4D44-9523-80865DF0B59D\/build\/resources\/main:\/Users\/ktoso\/.gradle\/caches\/modules-2\/files-2.1\/org.apache.commons\/commons-csv\/1.12.0\/c77e053d7189bc0857f8d323ab61cb949965fbd1\/commons-csv-1.12.0.jar:\/Users\/ktoso\/.gradle\/caches\/modules-2\/files-2.1\/commons-io\/commons-io\/2.17.0\/ddcc8433eb019fb48fe25207c0278143f3e1d7e2\/commons-io-2.17.0.jar:\/Users\/ktoso\/.gradle\/caches\/modules-2\/files-2.1\/commons-codec\/commons-codec\/1.17.1\/973638b7149d333563584137ebf13a691bb60579\/commons-codec-1.17.1.jar:test.jar:\/Users\/ktoso\/code\/swift-java\/Samples\/JavaDependencySampleApp\/.build\/swift-java-dependencies-C358EF3D-93BF-4D44-9523-80865DF0B59D\/build\/classes\/java\/main:\/Users\/ktoso\/code\/swift-java\/Samples\/JavaDependencySampleApp\/.build\/swift-java-dependencies-C358EF3D-93BF-4D44-9523-80865DF0B59D\/build\/resources\/main:\/Users\/ktoso\/.gradle\/caches\/modules-2\/files-2.1\/org.apache.commons\/commons-csv\/1.12.0\/c77e053d7189bc0857f8d323ab61cb949965fbd1\/commons-csv-1.12.0.jar:\/Users\/ktoso\/.gradle\/caches\/modules-2\/files-2.1\/commons-io\/commons-io\/2.17.0\/ddcc8433eb019fb48fe25207c0278143f3e1d7e2\/commons-io-2.17.0.jar:\/Users\/ktoso\/.gradle\/caches\/modules-2\/files-2.1\/commons-codec\/commons-codec\/1.17.1\/973638b7149d333563584137ebf13a691bb60579\/commons-codec-1.17.1.jar:\/Users\/ktoso\/code\/swift-java\/Samples\/JavaDependencySampleApp\/.build\/swift-java-dependencies-C358EF3D-93BF-4D44-9523-80865DF0B59D\/build\/classes\/java\/main:\/Users\/ktoso\/code\/swift-java\/Samples\/JavaDependencySampleApp\/.build\/swift-java-dependencies-C358EF3D-93BF-4D44-9523-80865DF0B59D\/build\/resources\/main:\/Users\/ktoso\/.gradle\/caches\/modules-2\/files-2.1\/org.apache.commons\/commons-csv\/1.12.0\/c77e053d7189bc0857f8d323ab61cb949965fbd1\/commons-csv-1.12.0.jar:\/Users\/ktoso\/.gradle\/caches\/modules-2\/files-2.1\/commons-io\/commons-io\/2.17.0\/ddcc8433eb019fb48fe25207c0278143f3e1d7e2\/commons-io-2.17.0.jar:\/Users\/ktoso\/.gradle\/caches\/modules-2\/files-2.1\/commons-codec\/commons-codec\/1.17.1\/973638b7149d333563584137ebf13a691bb60579\/commons-codec-1.17.1.jar:test.jar:\/Users\/ktoso\/code\/swift-java\/Samples\/JavaDependencySampleApp\/.build\/swift-java-dependencies-C358EF3D-93BF-4D44-9523-80865DF0B59D\/build\/classes\/java\/main:\/Users\/ktoso\/code\/swift-java\/Samples\/JavaDependencySampleApp\/.build\/swift-java-dependencies-C358EF3D-93BF-4D44-9523-80865DF0B59D\/build\/resources\/main:\/Users\/ktoso\/.gradle\/caches\/modules-2\/files-2.1\/org.apache.commons\/commons-csv\/1.12.0\/c77e053d7189bc0857f8d323ab61cb949965fbd1\/commons-csv-1.12.0.jar:\/Users\/ktoso\/.gradle\/caches\/modules-2\/files-2.1\/commons-io\/commons-io\/2.17.0\/ddcc8433eb019fb48fe25207c0278143f3e1d7e2\/commons-io-2.17.0.jar:\/Users\/ktoso\/.gradle\/caches\/modules-2\/files-2.1\/commons-codec\/commons-codec\/1.17.1\/973638b7149d333563584137ebf13a691bb60579\/commons-codec-1.17.1.jar:\/Users\/ktoso\/code\/swift-java\/Samples\/JavaDependencySampleApp\/.build\/swift-java-dependencies-C358EF3D-93BF-4D44-9523-80865DF0B59D\/build\/classes\/java\/main:\/Users\/ktoso\/code\/swift-java\/Samples\/JavaDependencySampleApp\/.build\/swift-java-dependencies-C358EF3D-93BF-4D44-9523-80865DF0B59D\/build\/resources\/main:\/Users\/ktoso\/.gradle\/caches\/modules-2\/files-2.1\/org.apache.commons\/commons-csv\/1.12.0\/c77e053d7189bc0857f8d323ab61cb949965fbd1\/commons-csv-1.12.0.jar:\/Users\/ktoso\/.gradle\/caches\/modules-2\/files-2.1\/commons-io\/commons-io\/2.17.0\/ddcc8433eb019fb48fe25207c0278143f3e1d7e2\/commons-io-2.17.0.jar:\/Users\/ktoso\/.gradle\/caches\/modules-2\/files-2.1\/commons-codec\/commons-codec\/1.17.1\/973638b7149d333563584137ebf13a691bb60579\/commons-codec-1.17.1.jar:test.jar:\/Users\/ktoso\/code\/swift-java\/Samples\/JavaDependencySampleApp\/.build\/swift-java-dependencies-C358EF3D-93BF-4D44-9523-80865DF0B59D\/build\/classes\/java\/main:\/Users\/ktoso\/code\/swift-java\/Samples\/JavaDependencySampleApp\/.build\/swift-java-dependencies-C358EF3D-93BF-4D44-9523-80865DF0B59D\/build\/resources\/main:\/Users\/ktoso\/.gradle\/caches\/modules-2\/files-2.1\/org.apache.commons\/commons-csv\/1.12.0\/c77e053d7189bc0857f8d323ab61cb949965fbd1\/commons-csv-1.12.0.jar:\/Users\/ktoso\/.gradle\/caches\/modules-2\/files-2.1\/commons-io\/commons-io\/2.17.0\/ddcc8433eb019fb48fe25207c0278143f3e1d7e2\/commons-io-2.17.0.jar:\/Users\/ktoso\/.gradle\/caches\/modules-2\/files-2.1\/commons-codec\/commons-codec\/1.17.1\/973638b7149d333563584137ebf13a691bb60579\/commons-codec-1.17.1.jar:\/Users\/ktoso\/code\/swift-java\/Samples\/JavaDependencySampleApp\/.build\/swift-java-dependencies-C358EF3D-93BF-4D44-9523-80865DF0B59D\/build\/classes\/java\/main:\/Users\/ktoso\/code\/swift-java\/Samples\/JavaDependencySampleApp\/.build\/swift-java-dependencies-C358EF3D-93BF-4D44-9523-80865DF0B59D\/build\/resources\/main:\/Users\/ktoso\/.gradle\/caches\/modules-2\/files-2.1\/org.apache.commons\/commons-csv\/1.12.0\/c77e053d7189bc0857f8d323ab61cb949965fbd1\/commons-csv-1.12.0.jar:\/Users\/ktoso\/.gradle\/caches\/modules-2\/files-2.1\/commons-io\/commons-io\/2.17.0\/ddcc8433eb019fb48fe25207c0278143f3e1d7e2\/commons-io-2.17.0.jar:\/Users\/ktoso\/.gradle\/caches\/modules-2\/files-2.1\/commons-codec\/commons-codec\/1.17.1\/973638b7149d333563584137ebf13a691bb60579\/commons-codec-1.17.1.jar:test.jar:\/Users\/ktoso\/code\/swift-java\/Samples\/JavaDependencySampleApp\/.build\/swift-java-dependencies-C358EF3D-93BF-4D44-9523-80865DF0B59D\/build\/classes\/java\/main:\/Users\/ktoso\/code\/swift-java\/Samples\/JavaDependencySampleApp\/.build\/swift-java-dependencies-C358EF3D-93BF-4D44-9523-80865DF0B59D\/build\/resources\/main:\/Users\/ktoso\/.gradle\/caches\/modules-2\/files-2.1\/org.apache.commons\/commons-csv\/1.12.0\/c77e053d7189bc0857f8d323ab61cb949965fbd1\/commons-csv-1.12.0.jar:\/Users\/ktoso\/.gradle\/caches\/modules-2\/files-2.1\/commons-io\/commons-io\/2.17.0\/ddcc8433eb019fb48fe25207c0278143f3e1d7e2\/commons-io-2.17.0.jar:\/Users\/ktoso\/.gradle\/caches\/modules-2\/files-2.1\/commons-codec\/commons-codec\/1.17.1\/973638b7149d333563584137ebf13a691bb60579\/commons-codec-1.17.1.jar:\/Users\/ktoso\/code\/swift-java\/Samples\/JavaDependencySampleApp\/.build\/swift-java-dependencies-C358EF3D-93BF-4D44-9523-80865DF0B59D\/build\/classes\/java\/main:\/Users\/ktoso\/code\/swift-java\/Samples\/JavaDependencySampleApp\/.build\/swift-java-dependencies-C358EF3D-93BF-4D44-9523-80865DF0B59D\/build\/resources\/main:\/Users\/ktoso\/.gradle\/caches\/modules-2\/files-2.1\/org.apache.commons\/commons-csv\/1.12.0\/c77e053d7189bc0857f8d323ab61cb949965fbd1\/commons-csv-1.12.0.jar:\/Users\/ktoso\/.gradle\/caches\/modules-2\/files-2.1\/commons-io\/commons-io\/2.17.0\/ddcc8433eb019fb48fe25207c0278143f3e1d7e2\/commons-io-2.17.0.jar:\/Users\/ktoso\/.gradle\/caches\/modules-2\/files-2.1\/commons-codec\/commons-codec\/1.17.1\/973638b7149d333563584137ebf13a691bb60579\/commons-codec-1.17.1.jar:test.jar:test.jar:test.jar:test.jar:test.jar:test.jar:configure:test.jar:configure" +} diff --git a/Samples/JavaDependencySampleApp/ci-validate.sh b/Samples/JavaDependencySampleApp/ci-validate.sh index 7012b8413..1c3e2d552 100755 --- a/Samples/JavaDependencySampleApp/ci-validate.sh +++ b/Samples/JavaDependencySampleApp/ci-validate.sh @@ -3,4 +3,25 @@ set -e set -x -swift run --disable-sandbox +# WORKAROUND: prebuilts broken on Swift 6.2.1 and Linux and tests using macros https://github.com/swiftlang/swift-java/issues/418 +if [ "$(uname)" = "Darwin" ]; then + DISABLE_EXPERIMENTAL_PREBUILTS='' +else + DISABLE_EXPERIMENTAL_PREBUILTS='--disable-experimental-prebuilts' +fi + +# invoke resolve as part of a build run +swift build \ + $DISABLE_EXPERIMENTAL_PREBUILTS \ + --disable-sandbox + +# explicitly invoke resolve without explicit path or dependency +# the dependencies should be uses from the --swift-module + +# FIXME: until prebuilt swift-syntax isn't broken on 6.2 anymore: https://github.com/swiftlang/swift-java/issues/418 +swift run \ + $DISABLE_EXPERIMENTAL_PREBUILTS \ + swift-java resolve \ + Sources/JavaCommonsCSV/swift-java.config \ + --swift-module JavaCommonsCSV \ + --output-directory .build/plugins/outputs/javadependencysampleapp/JavaCommonsCSV/destination/SwiftJavaPlugin/ diff --git a/Samples/JavaDependencySampleApp/gradle.properties b/Samples/JavaDependencySampleApp/gradle.properties new file mode 120000 index 000000000..7677fb73b --- /dev/null +++ b/Samples/JavaDependencySampleApp/gradle.properties @@ -0,0 +1 @@ +../gradle.properties \ No newline at end of file diff --git a/Samples/JavaKitSampleApp/Package.swift b/Samples/JavaKitSampleApp/Package.swift index 0956290c4..082d61ac9 100644 --- a/Samples/JavaKitSampleApp/Package.swift +++ b/Samples/JavaKitSampleApp/Package.swift @@ -35,19 +35,17 @@ let javaIncludePath = "\(javaHome)/include" let javaPlatformIncludePath = "\(javaIncludePath)/linux" #elseif os(macOS) let javaPlatformIncludePath = "\(javaIncludePath)/darwin" -#else - // TODO: Handle windows as well - #error("Currently only macOS and Linux platforms are supported, this may change in the future.") +#elseif os(Windows) + let javaPlatformIncludePath = "\(javaIncludePath)/win32)" #endif let package = Package( name: "JavaKitSampleApp", platforms: [ - .macOS(.v13), - .iOS(.v13), - .tvOS(.v13), - .watchOS(.v6), - .macCatalyst(.v13), + .macOS(.v15), + .iOS(.v18), + .watchOS(.v11), + .tvOS(.v18), ], products: [ @@ -66,9 +64,9 @@ let package = Package( .target( name: "JavaKitExample", dependencies: [ - .product(name: "JavaKit", package: "swift-java"), - .product(name: "JavaKitFunction", package: "swift-java"), - .product(name: "JavaKitJar", package: "swift-java"), + .product(name: "SwiftJava", package: "swift-java"), + .product(name: "JavaUtilFunction", package: "swift-java"), + .product(name: "JavaUtilJar", package: "swift-java"), ], swiftSettings: [ .swiftLanguageMode(.v5), @@ -80,5 +78,16 @@ let package = Package( .plugin(name: "SwiftJavaPlugin", package: "swift-java"), ] ), + + .testTarget( + name: "JavaKitExampleTests", + dependencies: [ + "JavaKitExample" + ], + swiftSettings: [ + .swiftLanguageMode(.v5), + .unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"]) + ] + ), ] ) diff --git a/Samples/JavaKitSampleApp/Sources/JavaKitExample/JavaKitExample.swift b/Samples/JavaKitSampleApp/Sources/JavaKitExample/JavaKitExample.swift index f074fee6a..44f811d30 100644 --- a/Samples/JavaKitSampleApp/Sources/JavaKitExample/JavaKitExample.swift +++ b/Samples/JavaKitSampleApp/Sources/JavaKitExample/JavaKitExample.swift @@ -12,8 +12,8 @@ // //===----------------------------------------------------------------------===// -import JavaKit -import JavaKitFunction +import SwiftJava +import JavaUtilFunction enum SwiftWrappedError: Error { case message(String) diff --git a/Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/ThreadSafeHelperClass.java b/Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/ThreadSafeHelperClass.java index 3b7793f07..38fe1a741 100644 --- a/Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/ThreadSafeHelperClass.java +++ b/Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/ThreadSafeHelperClass.java @@ -23,7 +23,7 @@ public class ThreadSafeHelperClass { public ThreadSafeHelperClass() { } - public Optional text = Optional.of(""); + public Optional text = Optional.of("cool string"); public final OptionalDouble val = OptionalDouble.of(2); @@ -31,6 +31,20 @@ public String getValue(Optional name) { return name.orElse(""); } + + public String getOrElse(Optional name) { + return name.orElse("or else value"); + } + + public Optional getNil() { + return Optional.empty(); + } + + // @NonNull + // public Optional getNil() { + // return Optional.empty(); + // } + public Optional getText() { return text; } diff --git a/Samples/JavaKitSampleApp/Tests/JavaKitExampleTests/JavaKitOptionalTests.swift b/Samples/JavaKitSampleApp/Tests/JavaKitExampleTests/JavaKitOptionalTests.swift new file mode 100644 index 000000000..9ed14cd05 --- /dev/null +++ b/Samples/JavaKitSampleApp/Tests/JavaKitExampleTests/JavaKitOptionalTests.swift @@ -0,0 +1,56 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JavaKitExample + +import SwiftJava +import JavaUtilFunction +import Testing + +@Suite +struct ManglingTests { + + @Test + func methodMangling() throws { + let jvm = try! JavaVirtualMachine.shared( + classpath: [ + ".build/plugins/outputs/javakitsampleapp/JavaKitExample/destination/JavaCompilerPlugin/Java" + ] + ) + let env = try! jvm.environment() + + let helper = ThreadSafeHelperClass(environment: env) + + let text: JavaString? = helper.textOptional + #expect(#"Optional("cool string")"# == String(describing: Optional("cool string"))) + #expect(#"Optional("cool string")"# == String(describing: text)) + + // let defaultValue: String? = helper.getOrElse(JavaOptional.empty()) + // #expect(#"Optional("or else value")"# == String(describing: defaultValue)) + + let noneValue: JavaOptional = helper.getNil()! + #expect(noneValue.isPresent() == false) + #expect("\(noneValue)" == "SwiftJava.JavaOptional") + + let textFunc: JavaString? = helper.getTextOptional() + #expect(#"Optional("cool string")"# == String(describing: textFunc)) + + let doubleOpt: Double? = helper.valOptional + #expect(#"Optional(2.0)"# == String(describing: doubleOpt)) + + let longOpt: Int64? = helper.fromOptional(21 as Int32?) + #expect(#"Optional(21)"# == String(describing: longOpt)) + } + +} \ No newline at end of file diff --git a/Samples/JavaKitSampleApp/ci-validate.sh b/Samples/JavaKitSampleApp/ci-validate.sh index f453a00be..327baadf9 100755 --- a/Samples/JavaKitSampleApp/ci-validate.sh +++ b/Samples/JavaKitSampleApp/ci-validate.sh @@ -3,7 +3,15 @@ set -e set -x -swift build +# WORKAROUND: prebuilts broken on Swift 6.2.1 and Linux and tests using macros https://github.com/swiftlang/swift-java/issues/418 +if [ "$(uname)" = "Darwin" ]; then + DISABLE_EXPERIMENTAL_PREBUILTS='' +else + DISABLE_EXPERIMENTAL_PREBUILTS='--disable-experimental-prebuilts' +fi + +swift build $DISABLE_EXPERIMENTAL_PREBUILTS + "$JAVA_HOME/bin/java" \ -cp .build/plugins/outputs/javakitsampleapp/JavaKitExample/destination/JavaCompilerPlugin/Java \ -Djava.library.path=.build/debug \ diff --git a/Samples/JavaKitSampleApp/gradle.properties b/Samples/JavaKitSampleApp/gradle.properties new file mode 120000 index 000000000..7677fb73b --- /dev/null +++ b/Samples/JavaKitSampleApp/gradle.properties @@ -0,0 +1 @@ +../gradle.properties \ No newline at end of file diff --git a/Samples/JavaProbablyPrime/Package.swift b/Samples/JavaProbablyPrime/Package.swift index 4cc887f84..3ebf8fcb5 100644 --- a/Samples/JavaProbablyPrime/Package.swift +++ b/Samples/JavaProbablyPrime/Package.swift @@ -7,7 +7,10 @@ import PackageDescription let package = Package( name: "JavaProbablyPrime", platforms: [ - .macOS(.v10_15), + .macOS(.v15), + .iOS(.v18), + .watchOS(.v11), + .tvOS(.v18), ], products: [ @@ -26,8 +29,8 @@ let package = Package( .executableTarget( name: "JavaProbablyPrime", dependencies: [ - .product(name: "JavaKitCollection", package: "swift-java"), - .product(name: "JavaKit", package: "swift-java"), + .product(name: "JavaUtil", package: "swift-java"), + .product(name: "SwiftJava", package: "swift-java"), .product(name: "ArgumentParser", package: "swift-argument-parser"), ], swiftSettings: [ diff --git a/Samples/JavaProbablyPrime/Sources/JavaProbablyPrime/prime.swift b/Samples/JavaProbablyPrime/Sources/JavaProbablyPrime/prime.swift index 07f701bdb..c070c87ad 100644 --- a/Samples/JavaProbablyPrime/Sources/JavaProbablyPrime/prime.swift +++ b/Samples/JavaProbablyPrime/Sources/JavaProbablyPrime/prime.swift @@ -13,7 +13,7 @@ //===----------------------------------------------------------------------===// import ArgumentParser -import JavaKit +import SwiftJava @main struct ProbablyPrime: ParsableCommand { diff --git a/Samples/JavaProbablyPrime/ci-validate.sh b/Samples/JavaProbablyPrime/ci-validate.sh index 0bdd86d17..202dcbabe 100755 --- a/Samples/JavaProbablyPrime/ci-validate.sh +++ b/Samples/JavaProbablyPrime/ci-validate.sh @@ -3,4 +3,13 @@ set -e set -x -swift run JavaProbablyPrime 1337 \ No newline at end of file +# WORKAROUND: prebuilts broken on Swift 6.2.1 and Linux and tests using macros https://github.com/swiftlang/swift-java/issues/418 +if [ "$(uname)" = "Darwin" ]; then + DISABLE_EXPERIMENTAL_PREBUILTS='' +else + DISABLE_EXPERIMENTAL_PREBUILTS='--disable-experimental-prebuilts' +fi + +swift run \ + $DISABLE_EXPERIMENTAL_PREBUILTS \ + JavaProbablyPrime 1337 \ No newline at end of file diff --git a/Samples/JavaProbablyPrime/gradle.properties b/Samples/JavaProbablyPrime/gradle.properties new file mode 120000 index 000000000..7677fb73b --- /dev/null +++ b/Samples/JavaProbablyPrime/gradle.properties @@ -0,0 +1 @@ +../gradle.properties \ No newline at end of file diff --git a/Samples/JavaSieve/Package.swift b/Samples/JavaSieve/Package.swift index cd65f82ef..c34d83318 100644 --- a/Samples/JavaSieve/Package.swift +++ b/Samples/JavaSieve/Package.swift @@ -42,7 +42,10 @@ let javaIncludePath = "\(javaHome)/include" let package = Package( name: "JavaSieve", platforms: [ - .macOS(.v10_15), + .macOS(.v15), + .iOS(.v18), + .watchOS(.v11), + .tvOS(.v18), ], dependencies: [ .package(name: "swift-java", path: "../../"), @@ -51,9 +54,10 @@ let package = Package( .target( name: "JavaMath", dependencies: [ - .product(name: "JavaKit", package: "swift-java"), - .product(name: "JavaKitJar", package: "swift-java"), + .product(name: "SwiftJava", package: "swift-java"), + .product(name: "JavaUtilJar", package: "swift-java"), ], + exclude: ["swift-java.config"], swiftSettings: [ .unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"]) ], @@ -68,9 +72,10 @@ let package = Package( name: "JavaSieve", dependencies: [ "JavaMath", - .product(name: "JavaKit", package: "swift-java"), - .product(name: "JavaKitCollection", package: "swift-java"), + .product(name: "SwiftJava", package: "swift-java"), + .product(name: "JavaUtil", package: "swift-java"), ], + exclude: ["swift-java.config"], swiftSettings: [ .unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"]) ], diff --git a/Samples/JavaSieve/Sources/JavaSieve/main.swift b/Samples/JavaSieve/Sources/JavaSieve/main.swift index e20477134..470ee5c79 100644 --- a/Samples/JavaSieve/Sources/JavaSieve/main.swift +++ b/Samples/JavaSieve/Sources/JavaSieve/main.swift @@ -12,13 +12,10 @@ // //===----------------------------------------------------------------------===// -import JavaKit +import SwiftJava import JavaMath -let jvm = try JavaVirtualMachine.shared(classpath: [ - "quadratic-sieve-Java/build/libs/QuadraticSieve-1.0.jar", - ".", -]) +let jvm = try JavaVirtualMachine.shared() do { let sieveClass = try JavaClass(environment: jvm.environment()) @@ -26,7 +23,7 @@ do { print("Found prime: \(prime.intValue())") } - try JavaClass().HALF_UP + _ = try JavaClass().HALF_UP // can import a Java enum value } catch { print("Failure: \(error)") } diff --git a/Samples/JavaSieve/Sources/JavaSieve/swift-java.config b/Samples/JavaSieve/Sources/JavaSieve/swift-java.config index 7e055d1c9..40d01d4b9 100644 --- a/Samples/JavaSieve/Sources/JavaSieve/swift-java.config +++ b/Samples/JavaSieve/Sources/JavaSieve/swift-java.config @@ -29,3 +29,4 @@ "com.gazman.quadratic_sieve.wheel.Wheel" : "Wheel" } } + diff --git a/Samples/SwiftAndJavaJarSampleLib/LICENSE.txt b/Samples/SwiftAndJavaJarSampleLib/LICENSE.txt new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/Samples/SwiftAndJavaJarSampleLib/LICENSE.txt @@ -0,0 +1,202 @@ + + 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 [yyyy] [name of copyright owner] + + 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. diff --git a/Samples/SwiftAndJavaJarSampleLib/Package.swift b/Samples/SwiftAndJavaJarSampleLib/Package.swift index 5f1322398..32ffbb287 100644 --- a/Samples/SwiftAndJavaJarSampleLib/Package.swift +++ b/Samples/SwiftAndJavaJarSampleLib/Package.swift @@ -43,7 +43,10 @@ let javaIncludePath = "\(javaHome)/include" let package = Package( name: "SwiftAndJavaJarSampleLib", platforms: [ - .macOS(.v10_15) + .macOS(.v15), + .iOS(.v18), + .watchOS(.v11), + .tvOS(.v18), ], products: [ .library( @@ -60,7 +63,7 @@ let package = Package( .target( name: "MySwiftLibrary", dependencies: [ - .product(name: "SwiftKitSwift", package: "swift-java"), + .product(name: "SwiftRuntimeFunctions", package: "swift-java"), ], exclude: [ "swift-java.config", diff --git a/Samples/SwiftAndJavaJarSampleLib/Sources/MySwiftLibrary/MySwiftClass.swift b/Samples/SwiftAndJavaJarSampleLib/Sources/MySwiftLibrary/MySwiftClass.swift new file mode 100644 index 000000000..c842715cd --- /dev/null +++ b/Samples/SwiftAndJavaJarSampleLib/Sources/MySwiftLibrary/MySwiftClass.swift @@ -0,0 +1,69 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// This is a "plain Swift" file containing various types of declarations, +// that is exported to Java by using the `jextract-swift` tool. +// +// No annotations are necessary on the Swift side to perform the export. + +#if os(Linux) +import Glibc +#else +import Darwin.C +#endif + +public class MySwiftClass { + + public var len: Int + public var cap: Int + + public init(len: Int, cap: Int) { + self.len = len + self.cap = cap + + p("\(MySwiftClass.self).len = \(self.len)") + p("\(MySwiftClass.self).cap = \(self.cap)") + let addr = unsafeBitCast(self, to: UInt64.self) + p("initializer done, self = 0x\(String(addr, radix: 16, uppercase: true))") + } + + deinit { + let addr = unsafeBitCast(self, to: UInt64.self) + p("Deinit, self = 0x\(String(addr, radix: 16, uppercase: true))") + } + + public var counter: Int32 = 0 + + public func voidMethod() { + p("") + } + + public func takeIntMethod(i: Int) { + p("i:\(i)") + } + + public func echoIntMethod(i: Int) -> Int { + p("i:\(i)") + return i + } + + public func makeIntMethod() -> Int { + p("make int -> 12") + return 12 + } + + public func makeRandomIntMethod() -> Int { + return Int.random(in: 1..<256) + } +} \ No newline at end of file diff --git a/Samples/SwiftAndJavaJarSampleLib/Sources/MySwiftLibrary/MySwiftLibrary.swift b/Samples/SwiftAndJavaJarSampleLib/Sources/MySwiftLibrary/MySwiftLibrary.swift index 84e4618fd..e900fdd0f 100644 --- a/Samples/SwiftAndJavaJarSampleLib/Sources/MySwiftLibrary/MySwiftLibrary.swift +++ b/Samples/SwiftAndJavaJarSampleLib/Sources/MySwiftLibrary/MySwiftLibrary.swift @@ -39,63 +39,10 @@ public func globalCallMeRunnable(run: () -> ()) { run() } -public class MySwiftClass { - - public var len: Int - public var cap: Int - - public init(len: Int, cap: Int) { - self.len = len - self.cap = cap - - p("\(MySwiftClass.self).len = \(self.len)") - p("\(MySwiftClass.self).cap = \(self.cap)") - let addr = unsafeBitCast(self, to: UInt64.self) - p("initializer done, self = 0x\(String(addr, radix: 16, uppercase: true))") - } - - deinit { - let addr = unsafeBitCast(self, to: UInt64.self) - p("Deinit, self = 0x\(String(addr, radix: 16, uppercase: true))") - } - - public var counter: Int32 = 0 - - public func voidMethod() { - p("") - } - - public func takeIntMethod(i: Int) { - p("i:\(i)") - } - - public func echoIntMethod(i: Int) -> Int { - p("i:\(i)") - return i - } - - public func makeIntMethod() -> Int { - p("make int -> 12") - return 12 - } - - public func makeRandomIntMethod() -> Int { - return Int.random(in: 1..<256) - } -} - // ==== Internal helpers -private func p(_ msg: String, file: String = #fileID, line: UInt = #line, function: String = #function) { +func p(_ msg: String, file: String = #fileID, line: UInt = #line, function: String = #function) { print("[swift][\(file):\(line)](\(function)) \(msg)") fflush(stdout) } -#if os(Linux) -// FIXME: why do we need this workaround? -@_silgen_name("_objc_autoreleaseReturnValue") -public func _objc_autoreleaseReturnValue(a: Any) {} - -@_silgen_name("objc_autoreleaseReturnValue") -public func objc_autoreleaseReturnValue(a: Any) {} -#endif diff --git a/Samples/SwiftAndJavaJarSampleLib/build.gradle b/Samples/SwiftAndJavaJarSampleLib/build.gradle index 2125011db..e6404adb1 100644 --- a/Samples/SwiftAndJavaJarSampleLib/build.gradle +++ b/Samples/SwiftAndJavaJarSampleLib/build.gradle @@ -38,49 +38,69 @@ repositories { java { toolchain { - languageVersion.set(JavaLanguageVersion.of(22)) + languageVersion.set(JavaLanguageVersion.of(25)) } } dependencies { - implementation(project(':SwiftKit')) + implementation(project(':SwiftKitCore')) + implementation(project(':SwiftKitFFM')) + testRuntimeOnly("org.junit.platform:junit-platform-launcher") // TODO: workaround for not finding junit: https://github.com/gradle/gradle/issues/34512 testImplementation(platform("org.junit:junit-bom:5.10.0")) testImplementation("org.junit.jupiter:junit-jupiter") } -// This is for development, when we edit the Swift swift-java project, the outputs of the generated sources may change. -// Thus, we also need to watch and re-build the top level project. -def compileSwiftJExtractPlugin = tasks.register("compileSwiftJExtractPlugin", Exec) { - description = "Rebuild the swift-java root project" +def swiftProductsWithJExtractPlugin() { + def stdout = new ByteArrayOutputStream() + def stderr = new ByteArrayOutputStream() - inputs.file(new File(rootDir, "Package.swift")) - inputs.dir(new File(rootDir, "Sources")) - outputs.dir(new File(rootDir, ".build")) + def processBuilder = new ProcessBuilder('swift', 'package', 'describe', '--type', 'json') + def process = processBuilder.start() + + process.consumeProcessOutput(stdout, stderr) + process.waitFor() - workingDir = rootDir + def exitValue = process.exitValue() + def jsonOutput = stdout.toString() + + if (exitValue == 0) { + def json = new JsonSlurper().parseText(jsonOutput) + def products = json.targets + .findAll { target -> + target.product_dependencies?.contains("JExtractSwiftPlugin") + } + .collectMany { target -> + target.product_memberships ?: [] + } + return products + } else { + logger.warn("Command failed: ${stderr.toString()}") + return [] + } +} + +def swiftCheckValid = tasks.register("swift-check-valid", Exec) { commandLine "swift" - args("build", - "-c", swiftBuildConfiguration(), - "--product", "SwiftKitSwift", - "--product", "JExtractSwiftPlugin", - "--product", "JExtractSwiftCommandPlugin") + args("-version") } def jextract = tasks.register("jextract", Exec) { - description = "Builds swift sources, including swift-java source generation" - dependsOn compileSwiftJExtractPlugin + description = "Generate Java wrappers for swift target" + dependsOn swiftCheckValid // only because we depend on "live developing" the plugin while using this project to test it inputs.file(new File(rootDir, "Package.swift")) inputs.dir(new File(rootDir, "Sources")) + // If the package description changes, we should execute jextract again, maybe we added jextract to new targets inputs.file(new File(projectDir, "Package.swift")) - inputs.dir(new File(projectDir, "Sources")) - // TODO: we can use package describe --type json to figure out which targets depend on JExtractSwiftPlugin and will produce outputs - // Avoid adding this directory, but create the expected one specifically for all targets - // which WILL produce sources because they have the plugin + // monitor all targets/products which depend on the JExtract plugin + swiftProductsWithJExtractPlugin().each { + logger.info("[swift-java:jextract (Gradle)] Swift input target: ${it}") + inputs.dir(new File(layout.projectDirectory.asFile, "Sources/${it}".toString())) + } outputs.dir(layout.buildDirectory.dir("../.build/plugins/outputs/${layout.projectDirectory.asFile.getName().toLowerCase()}")) File baseSwiftPluginOutputsDir = layout.buildDirectory.dir("../.build/plugins/outputs/").get().asFile @@ -96,7 +116,17 @@ def jextract = tasks.register("jextract", Exec) { workingDir = layout.projectDirectory commandLine "swift" - args("package", "jextract", "-v", "--log-level", "info") // TODO: pass log level from Gradle build + // FIXME: disable prebuilts until swift-syntax isn't broken on 6.2 anymore: https://github.com/swiftlang/swift-java/issues/418 + args("build", "--disable-experimental-prebuilts") // since Swift targets which need to be jextract-ed have the jextract build plugin, we just need to build + // If we wanted to execute a specific subcommand, we can like this: + // args("run",/* + // "swift-java", "jextract", + // "--swift-module", "MySwiftLibrary", + // // java.package is obtained from the swift-java.config in the swift module + // "--output-java", "${layout.buildDirectory.dir(".build/plugins/outputs/${layout.projectDirectory.asFile.getName().toLowerCase()}/JExtractSwiftPlugin/src/generated/java").get()}", + // "--output-swift", "${layout.buildDirectory.dir(".build/plugins/outputs/${layout.projectDirectory.asFile.getName().toLowerCase()}/JExtractSwiftPlugin/Sources").get()}", + // "--log-level", (logging.level <= LogLevel.INFO ? "debug" : */"info") + // ) } @@ -126,7 +156,6 @@ tasks.named('test', Test) { // ==== Jar publishing List swiftProductDylibPaths() { - def process = ['swift', 'package', 'describe', '--type', 'json'].execute() process.waitFor() @@ -143,8 +172,6 @@ List swiftProductDylibPaths() { target.product_memberships }.flatten() - - def productDylibPaths = products.collect { logger.info("[swift-java] Include Swift product: '${it}' in product resource paths.") "${layout.projectDirectory}/.build/${swiftBuildConfiguration()}/lib${it}.dylib" diff --git a/Samples/SwiftAndJavaJarSampleLib/ci-validate.sh b/Samples/SwiftAndJavaJarSampleLib/ci-validate.sh new file mode 100755 index 000000000..2daddc615 --- /dev/null +++ b/Samples/SwiftAndJavaJarSampleLib/ci-validate.sh @@ -0,0 +1,68 @@ +#!/bin/sh + +set -e +set -x + +./gradlew jar + +SWIFT_VERSION="$(swift -version | awk '/Swift version/ { print $3 }')" + +# This is how env variables are set by setup-java +if [ "$(uname -m)" = 'arm64' ]; then + ARCH=ARM64 + JAVAC="${JAVA_HOME_25_ARM64}/bin/javac" + JAVA="${JAVA_HOME_25_ARM64}/bin/java" +else + ARCH=X64 + JAVAC="${JAVA_HOME_25_X64}/bin/javac" + JAVA="${JAVA_HOME_25_X64}/bin/java" +fi + +if [ -n "$JAVA_HOME_25_$ARCH" ]; then + export JAVA_HOME="$JAVA_HOME_25_$ARCH" +elif [ "$(uname -s)" = 'Linux' ] +then + export PATH="${PATH}:/usr/lib/jvm/jdk-25/bin" # we need to make sure to use the latest JDK to actually compile/run the executable +fi + +# check if we can compile a plain Example file that uses the generated Java bindings that should be in the generated jar +# The classpath MUST end with a * if it contains jar files, and must not if it directly contains class files. +SWIFTKIT_CORE_CLASSPATH="$(pwd)/../../SwiftKitCore/build/libs/*" +SWIFTKIT_FFM_CLASSPATH="$(pwd)/../../SwiftKitFFM/build/libs/*" +MYLIB_CLASSPATH="$(pwd)/build/libs/*" +CLASSPATH="$(pwd)/:${SWIFTKIT_FFM_CLASSPATH}:${SWIFTKIT_CORE_CLASSPATH}:${MYLIB_CLASSPATH}" +echo "CLASSPATH = ${CLASSPATH}" + +$JAVAC -cp "${CLASSPATH}" Example.java + +# FIXME: move all this into Gradle or SwiftPM and make it easier to get the right classpath for running +if [ "$(uname -s)" = 'Linux' ] +then + SWIFT_LIB_PATHS=/usr/lib/swift/linux + SWIFT_LIB_PATHS="${SWIFT_LIB_PATHS}:$(find . | grep libMySwiftLibrary.so$ | sort | head -n1 | xargs dirname)" + + # if we are on linux, find the Swiftly or System-wide installed libraries dir + SWIFT_CORE_LIB=$(find "$HOME"/.local -name "libswiftCore.so" 2>/dev/null | grep "$SWIFT_VERSION" | head -n1) + if [ -n "$SWIFT_CORE_LIB" ]; then + SWIFT_LIB_PATHS="${SWIFT_LIB_PATHS}:$(dirname "$SWIFT_CORE_LIB")" + ls "$SWIFT_LIB_PATHS" + else + # maybe there is one installed system-wide in /usr/lib? + SWIFT_CORE_LIB2=$(find /usr/lib -name "libswiftCore.so" 2>/dev/null | grep "$SWIFT_VERSION" | head -n1) + if [ -n "$SWIFT_CORE_LIB2" ]; then + SWIFT_LIB_PATHS="${SWIFT_LIB_PATHS}:$(dirname "$SWIFT_CORE_LIB2")" + fi + fi +elif [ "$(uname -s)" = 'Darwin' ] +then + SWIFT_LIB_PATHS=$(find "$(swiftly use --print-location)" | grep dylib$ | grep libswiftCore | grep macos | head -n1 | xargs dirname) + SWIFT_LIB_PATHS="${SWIFT_LIB_PATHS}:$(pwd)/$(find . | grep libMySwiftLibrary.dylib$ | sort | head -n1 | xargs dirname)" + +fi +echo "SWIFT_LIB_PATHS = ${SWIFT_LIB_PATHS}" + +# Can we run the example? +${JAVA} --enable-native-access=ALL-UNNAMED \ + -Djava.library.path="${SWIFT_LIB_PATHS}" \ + -cp "${CLASSPATH}" \ + Example diff --git a/Samples/SwiftAndJavaJarSampleLib/gradle.properties b/Samples/SwiftAndJavaJarSampleLib/gradle.properties new file mode 120000 index 000000000..7677fb73b --- /dev/null +++ b/Samples/SwiftAndJavaJarSampleLib/gradle.properties @@ -0,0 +1 @@ +../gradle.properties \ No newline at end of file diff --git a/Samples/SwiftAndJavaJarSampleLib/src/jmh/java/org/swift/swiftkit/JavaToSwiftBenchmark.java b/Samples/SwiftAndJavaJarSampleLib/src/jmh/java/org/swift/swiftkit/ffm/JavaToSwiftBenchmark.java similarity index 98% rename from Samples/SwiftAndJavaJarSampleLib/src/jmh/java/org/swift/swiftkit/JavaToSwiftBenchmark.java rename to Samples/SwiftAndJavaJarSampleLib/src/jmh/java/org/swift/swiftkit/ffm/JavaToSwiftBenchmark.java index 4e3edf03e..7d6b92dce 100644 --- a/Samples/SwiftAndJavaJarSampleLib/src/jmh/java/org/swift/swiftkit/JavaToSwiftBenchmark.java +++ b/Samples/SwiftAndJavaJarSampleLib/src/jmh/java/org/swift/swiftkit/ffm/JavaToSwiftBenchmark.java @@ -12,7 +12,7 @@ // //===----------------------------------------------------------------------===// -package org.swift.swiftkit; +package org.swift.swiftkit.ffm; import java.util.concurrent.TimeUnit; diff --git a/Samples/SwiftAndJavaJarSampleLib/src/main/java/com/example/swift/HelloJava2Swift.java b/Samples/SwiftAndJavaJarSampleLib/src/main/java/com/example/swift/HelloJava2Swift.java index cd8af700f..e41547bd8 100644 --- a/Samples/SwiftAndJavaJarSampleLib/src/main/java/com/example/swift/HelloJava2Swift.java +++ b/Samples/SwiftAndJavaJarSampleLib/src/main/java/com/example/swift/HelloJava2Swift.java @@ -16,15 +16,10 @@ // Import swift-extract generated sources -import com.example.swift.MySwiftLibrary; -import com.example.swift.MySwiftClass; - // Import javakit/swiftkit support libraries -import org.swift.swiftkit.SwiftArena; -import org.swift.swiftkit.SwiftKit; -import org.swift.swiftkit.SwiftValueWitnessTable; - -import java.util.Arrays; +import org.swift.swiftkit.core.SwiftLibraries; +import org.swift.swiftkit.ffm.AllocatingSwiftArena; +import org.swift.swiftkit.ffm.SwiftRuntime; public class HelloJava2Swift { @@ -32,7 +27,7 @@ public static void main(String[] args) { boolean traceDowncalls = Boolean.getBoolean("jextract.trace.downcalls"); System.out.println("Property: jextract.trace.downcalls = " + traceDowncalls); - System.out.print("Property: java.library.path = " +SwiftKit.getJavaLibraryPath()); + System.out.print("Property: java.library.path = " + SwiftLibraries.getJavaLibraryPath()); examples(); } @@ -43,12 +38,12 @@ static void examples() { MySwiftLibrary.globalTakeInt(1337); // Example of using an arena; MyClass.deinit is run at end of scope - try (var arena = SwiftArena.ofConfined()) { + try (var arena = AllocatingSwiftArena.ofConfined()) { MySwiftClass obj = MySwiftClass.init(2222, 7777, arena); // just checking retains/releases work - SwiftKit.retain(obj); - SwiftKit.release(obj); + SwiftRuntime.retain(obj); + SwiftRuntime.release(obj); obj.voidMethod(); obj.takeIntMethod(42); diff --git a/Samples/SwiftAndJavaJarSampleLib/src/test/java/com/example/swift/MySwiftClassTest.java b/Samples/SwiftAndJavaJarSampleLib/src/test/java/com/example/swift/MySwiftClassTest.java index bb46ef3da..6fb7651cf 100644 --- a/Samples/SwiftAndJavaJarSampleLib/src/test/java/com/example/swift/MySwiftClassTest.java +++ b/Samples/SwiftAndJavaJarSampleLib/src/test/java/com/example/swift/MySwiftClassTest.java @@ -16,8 +16,8 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import org.swift.swiftkit.SwiftArena; -import org.swift.swiftkit.SwiftKit; +import org.swift.swiftkit.core.SwiftLibraries; +import org.swift.swiftkit.ffm.AllocatingSwiftArena; import java.io.File; import java.util.stream.Stream; @@ -27,7 +27,7 @@ public class MySwiftClassTest { void checkPaths(Throwable throwable) { - var paths = SwiftKit.getJavaLibraryPath().split(":"); + var paths = SwiftLibraries.getJavaLibraryPath().split(":"); for (var path : paths) { System.out.println("CHECKING PATH: " + path); Stream.of(new File(path).listFiles()) @@ -42,7 +42,7 @@ void checkPaths(Throwable throwable) { @Test void test_MySwiftClass_voidMethod() { - try(var arena = SwiftArena.ofConfined()) { + try(var arena = AllocatingSwiftArena.ofConfined()) { MySwiftClass o = MySwiftClass.init(12, 42, arena); o.voidMethod(); } catch (Throwable throwable) { @@ -52,7 +52,7 @@ void test_MySwiftClass_voidMethod() { @Test void test_MySwiftClass_makeIntMethod() { - try(var arena = SwiftArena.ofConfined()) { + try(var arena = AllocatingSwiftArena.ofConfined()) { MySwiftClass o = MySwiftClass.init(12, 42, arena); var got = o.makeIntMethod(); assertEquals(12, got); @@ -62,7 +62,7 @@ void test_MySwiftClass_makeIntMethod() { @Test @Disabled // TODO: Need var mangled names in interfaces void test_MySwiftClass_property_len() { - try(var arena = SwiftArena.ofConfined()) { + try(var arena = AllocatingSwiftArena.ofConfined()) { MySwiftClass o = MySwiftClass.init(12, 42, arena); var got = o.getLen(); assertEquals(12, got); diff --git a/Samples/SwiftAndJavaJarSampleLib/src/test/java/com/example/swift/MySwiftLibraryTest.java b/Samples/SwiftAndJavaJarSampleLib/src/test/java/com/example/swift/MySwiftLibraryTest.java index 74df5da89..13ebb1b01 100644 --- a/Samples/SwiftAndJavaJarSampleLib/src/test/java/com/example/swift/MySwiftLibraryTest.java +++ b/Samples/SwiftAndJavaJarSampleLib/src/test/java/com/example/swift/MySwiftLibraryTest.java @@ -14,14 +14,10 @@ package com.example.swift; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import org.swift.swiftkit.SwiftKit; -import java.util.Arrays; import java.util.concurrent.CountDownLatch; -import java.util.stream.Collectors; import static org.junit.jupiter.api.Assertions.*; diff --git a/Samples/SwiftAndJavaJarSampleLib/src/test/java/org/swift/swiftkit/MySwiftClassTest.java b/Samples/SwiftAndJavaJarSampleLib/src/test/java/org/swift/swiftkit/ffm/MySwiftClassTest.java similarity index 72% rename from Samples/SwiftAndJavaJarSampleLib/src/test/java/org/swift/swiftkit/MySwiftClassTest.java rename to Samples/SwiftAndJavaJarSampleLib/src/test/java/org/swift/swiftkit/ffm/MySwiftClassTest.java index 78da5a642..fb1c5d1aa 100644 --- a/Samples/SwiftAndJavaJarSampleLib/src/test/java/org/swift/swiftkit/MySwiftClassTest.java +++ b/Samples/SwiftAndJavaJarSampleLib/src/test/java/org/swift/swiftkit/ffm/MySwiftClassTest.java @@ -12,7 +12,7 @@ // //===----------------------------------------------------------------------===// -package org.swift.swiftkit; +package org.swift.swiftkit.ffm; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -24,16 +24,16 @@ public class MySwiftClassTest { @Test void call_retain_retainCount_release() { - var arena = SwiftArena.ofConfined(); + var arena = AllocatingSwiftArena.ofConfined(); var obj = MySwiftClass.init(1, 2, arena); - assertEquals(1, SwiftKit.retainCount(obj)); + assertEquals(1, SwiftRuntime.retainCount(obj)); // TODO: test directly on SwiftHeapObject inheriting obj - SwiftKit.retain(obj); - assertEquals(2, SwiftKit.retainCount(obj)); + SwiftRuntime.retain(obj); + assertEquals(2, SwiftRuntime.retainCount(obj)); - SwiftKit.release(obj); - assertEquals(1, SwiftKit.retainCount(obj)); + SwiftRuntime.release(obj); + assertEquals(1, SwiftRuntime.retainCount(obj)); } } diff --git a/Samples/SwiftAndJavaJarSampleLib/src/test/java/org/swift/swiftkit/SwiftArenaTest.java b/Samples/SwiftAndJavaJarSampleLib/src/test/java/org/swift/swiftkit/ffm/SwiftArenaTest.java similarity index 77% rename from Samples/SwiftAndJavaJarSampleLib/src/test/java/org/swift/swiftkit/SwiftArenaTest.java rename to Samples/SwiftAndJavaJarSampleLib/src/test/java/org/swift/swiftkit/ffm/SwiftArenaTest.java index e8b1ac04f..8d786d952 100644 --- a/Samples/SwiftAndJavaJarSampleLib/src/test/java/org/swift/swiftkit/SwiftArenaTest.java +++ b/Samples/SwiftAndJavaJarSampleLib/src/test/java/org/swift/swiftkit/ffm/SwiftArenaTest.java @@ -12,20 +12,15 @@ // //===----------------------------------------------------------------------===// -package org.swift.swiftkit; +package org.swift.swiftkit.ffm; import com.example.swift.MySwiftClass; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledIf; -import org.swift.swiftkit.util.PlatformUtils; +import org.swift.swiftkit.core.util.PlatformUtils; -import java.util.Arrays; -import java.util.stream.Collectors; - -import static org.junit.jupiter.api.Assertions.*; -import static org.swift.swiftkit.SwiftKit.*; -import static org.swift.swiftkit.SwiftKit.retainCount; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.swift.swiftkit.ffm.SwiftRuntime.*; public class SwiftArenaTest { @@ -38,7 +33,7 @@ static boolean isAmd64() { @Test @DisabledIf("isAmd64") public void arena_releaseClassOnClose_class_ok() { - try (var arena = SwiftArena.ofConfined()) { + try (var arena = AllocatingSwiftArena.ofConfined()) { var obj = MySwiftClass.init(1, 2, arena); retain(obj); diff --git a/Samples/SwiftKitSampleApp/Package.swift b/Samples/SwiftJavaExtractFFMSampleApp/Package.swift similarity index 86% rename from Samples/SwiftKitSampleApp/Package.swift rename to Samples/SwiftJavaExtractFFMSampleApp/Package.swift index 65de7d3af..98d1bd33f 100644 --- a/Samples/SwiftKitSampleApp/Package.swift +++ b/Samples/SwiftJavaExtractFFMSampleApp/Package.swift @@ -41,9 +41,12 @@ let javaIncludePath = "\(javaHome)/include" #endif let package = Package( - name: "SwiftKitSampleApp", + name: "SwiftJavaExtractFFMSampleApp", platforms: [ - .macOS(.v10_15) + .macOS(.v15), + .iOS(.v18), + .watchOS(.v11), + .tvOS(.v18), ], products: [ .library( @@ -60,9 +63,9 @@ let package = Package( .target( name: "MySwiftLibrary", dependencies: [ - .product(name: "JavaKit", package: "swift-java"), - .product(name: "JavaRuntime", package: "swift-java"), - .product(name: "SwiftKitSwift", package: "swift-java"), + .product(name: "SwiftJava", package: "swift-java"), + .product(name: "CSwiftJavaJNI", package: "swift-java"), + .product(name: "SwiftRuntimeFunctions", package: "swift-java"), ], exclude: [ "swift-java.config", diff --git a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MultiplePublicTypes.swift b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MultiplePublicTypes.swift new file mode 100644 index 000000000..234cb357e --- /dev/null +++ b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MultiplePublicTypes.swift @@ -0,0 +1,26 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// This file exists to exercise the swiftpm plugin generating separate output Java files +// for the public types; because Java public types must be in a file with the same name as the type. + +public struct PublicTypeOne { + public init() {} + public func test() {} +} + +public struct PublicTypeTwo { + public init() {} + public func test() {} +} \ No newline at end of file diff --git a/Samples/SwiftKitSampleApp/Sources/MySwiftLibrary/MySwiftClass.swift b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftClass.swift similarity index 84% rename from Samples/SwiftKitSampleApp/Sources/MySwiftLibrary/MySwiftClass.swift rename to Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftClass.swift index 97f5149ec..e1139c2b3 100644 --- a/Samples/SwiftKitSampleApp/Sources/MySwiftLibrary/MySwiftClass.swift +++ b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftClass.swift @@ -14,6 +14,7 @@ public class MySwiftClass { + public let byte: UInt8 = 0 public var len: Int public var cap: Int @@ -59,4 +60,16 @@ public class MySwiftClass { public func makeRandomIntMethod() -> Int { return Int.random(in: 1..<256) } + + public func takeUnsignedChar(arg: UInt16) { + p("\(UInt32.self) = \(arg)") + } + + public func takeUnsignedInt(arg: UInt32) { + p("\(UInt32.self) = \(arg)") + } + + public func takeUnsignedLong(arg: UInt64) { + p("\(UInt64.self) = \(arg)") + } } diff --git a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift new file mode 100644 index 000000000..9929f888a --- /dev/null +++ b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift @@ -0,0 +1,120 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// This is a "plain Swift" file containing various types of declarations, +// that is exported to Java by using the `jextract-swift` tool. +// +// No annotations are necessary on the Swift side to perform the export. + +#if os(Linux) +import Glibc +#else +import Darwin.C +#endif + +import Foundation + +public func helloWorld() { + p("\(#function)") +} + +public func globalTakeInt(i: Int) { + p("i:\(i)") +} + +public func globalMakeInt() -> Int { + return 42 +} + +public func globalWriteString(string: String) -> Int { + return string.count +} + +public func globalTakeIntInt(i: Int, j: Int) { + p("i:\(i), j:\(j)") +} + +public func globalCallMeRunnable(run: () -> ()) { + run() +} + +public func globalReceiveRawBuffer(buf: UnsafeRawBufferPointer) -> Int { + return buf.count +} + +public var globalBuffer: UnsafeRawBufferPointer = UnsafeRawBufferPointer(UnsafeMutableRawBufferPointer.allocate(byteCount: 124, alignment: 1)) + +public func globalReceiveReturnData(data: Data) -> Data { + return Data(data) +} + +public func withBuffer(body: (UnsafeRawBufferPointer) -> Void) { + body(globalBuffer) +} + +public func getArray() -> [UInt8] { + return [1, 2, 3] +} + +public func sumAllByteArrayElements(actuallyAnArray: UnsafeRawPointer, count: Int) -> Int { + let bufferPointer = UnsafeRawBufferPointer(start: actuallyAnArray, count: count) + let array = Array(bufferPointer) + return Int(array.reduce(0, { partialResult, element in partialResult + element })) +} + +public func sumAllByteArrayElements(array: [UInt8]) -> Int { + return Int(array.reduce(0, { partialResult, element in partialResult + element })) +} + +public func withArray(body: ([UInt8]) -> Void) { + body([1, 2, 3]) +} + +public func globalReceiveSomeDataProtocol(data: some DataProtocol) -> Int { + p(Array(data).description) + return data.count +} + +public func globalReceiveOptional(o1: Int?, o2: (some DataProtocol)?) -> Int { + switch (o1, o2) { + case (nil, nil): + p(", ") + return 0 + case (let v1?, nil): + p("\(v1), ") + return 1 + case (nil, let v2?): + p(", \(v2)") + return 2 + case (let v1?, let v2?): + p("\(v1), \(v2)") + return 3 + } +} + +// ==== Internal helpers + +func p(_ msg: String, file: String = #fileID, line: UInt = #line, function: String = #function) { + print("[swift][\(file):\(line)](\(function)) \(msg)") + fflush(stdout) +} + + #if os(Linux) + // FIXME: why do we need this workaround? + @_silgen_name("_objc_autoreleaseReturnValue") + public func _objc_autoreleaseReturnValue(a: Any) {} + + @_silgen_name("objc_autoreleaseReturnValue") + public func objc_autoreleaseReturnValue(a: Any) {} + #endif diff --git a/Samples/SwiftKitSampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift similarity index 70% rename from Samples/SwiftKitSampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift rename to Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift index 363e06834..5b5c2d322 100644 --- a/Samples/SwiftKitSampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift +++ b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift @@ -16,10 +16,14 @@ public struct MySwiftStruct { private var cap: Int private var len: Int + private var subscriptValue: Int + private var subscriptArray: [Int] public init(cap: Int, len: Int) { self.cap = cap self.len = len + self.subscriptValue = 0 + self.subscriptArray = [10, 20, 15, 75] } public func voidMethod() { @@ -61,4 +65,22 @@ public struct MySwiftStruct { public func makeRandomIntMethod() -> Int { return Int.random(in: 1..<256) } + + public func getSubscriptValue() -> Int { + return self.subscriptValue + } + + public func getSubscriptArrayValue(index: Int) -> Int { + return self.subscriptArray[index] + } + + public subscript() -> Int { + get { return subscriptValue } + set { subscriptValue = newValue } + } + + public subscript(index: Int) -> Int { + get { return subscriptArray[index] } + set { subscriptArray[index] = newValue } + } } diff --git a/Samples/SwiftKitSampleApp/Sources/MySwiftLibrary/jni/JNIImplementations.swift b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/jni/JNIImplementations.swift similarity index 97% rename from Samples/SwiftKitSampleApp/Sources/MySwiftLibrary/jni/JNIImplementations.swift rename to Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/jni/JNIImplementations.swift index ef0e2e582..50561d328 100644 --- a/Samples/SwiftKitSampleApp/Sources/MySwiftLibrary/jni/JNIImplementations.swift +++ b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/jni/JNIImplementations.swift @@ -12,8 +12,8 @@ // //===----------------------------------------------------------------------===// -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaClass("com.example.swift.HelloJava2Swift") open class HelloJava2Swift: JavaObject { diff --git a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/swift-java.config b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/swift-java.config new file mode 100644 index 000000000..3a76a8d89 --- /dev/null +++ b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/swift-java.config @@ -0,0 +1,4 @@ +{ + "javaPackage": "com.example.swift", + "logLevel": "trace" +} diff --git a/Samples/SwiftKitSampleApp/build.gradle b/Samples/SwiftJavaExtractFFMSampleApp/build.gradle similarity index 53% rename from Samples/SwiftKitSampleApp/build.gradle rename to Samples/SwiftJavaExtractFFMSampleApp/build.gradle index 18a55f445..dcfacdd39 100644 --- a/Samples/SwiftKitSampleApp/build.gradle +++ b/Samples/SwiftJavaExtractFFMSampleApp/build.gradle @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import groovy.json.JsonSlurper import org.swift.swiftkit.gradle.BuildUtils import java.nio.file.* @@ -30,42 +31,61 @@ repositories { java { toolchain { - languageVersion.set(JavaLanguageVersion.of(22)) + languageVersion.set(JavaLanguageVersion.of(25)) } } +def swiftProductsWithJExtractPlugin() { + def stdout = new ByteArrayOutputStream() + def stderr = new ByteArrayOutputStream() + + def processBuilder = new ProcessBuilder('swift', 'package', 'describe', '--type', 'json') + def process = processBuilder.start() + + process.consumeProcessOutput(stdout, stderr) + process.waitFor() + + def exitValue = process.exitValue() + def jsonOutput = stdout.toString() + + if (exitValue == 0) { + def json = new JsonSlurper().parseText(jsonOutput) + def products = json.targets + .findAll { target -> + target.product_dependencies?.contains("JExtractSwiftPlugin") + } + .collectMany { target -> + target.product_memberships ?: [] + } + return products + } else { + logger.warn("Command failed: ${stderr.toString()}") + return [] + } +} -// This is for development, when we edit the Swift swift-java project, the outputs of the generated sources may change. -// Thus, we also need to watch and re-build the top level project. -def compileSwiftJExtractPlugin = tasks.register("compileSwiftJExtractPlugin", Exec) { - description = "Rebuild the swift-java root project" - - inputs.file(new File(rootDir, "Package.swift")) - inputs.dir(new File(rootDir, "Sources")) - outputs.dir(new File(rootDir, ".build")) - workingDir = rootDir +def swiftCheckValid = tasks.register("swift-check-valid", Exec) { commandLine "swift" - args("build", - "--product", "SwiftKitSwift", - "--product", "JExtractSwiftPlugin", - "--product", "JExtractSwiftCommandPlugin") + args("-version") } def jextract = tasks.register("jextract", Exec) { - description = "Builds swift sources, including swift-java source generation" - dependsOn compileSwiftJExtractPlugin + description = "Generate Java wrappers for swift target" + dependsOn swiftCheckValid // only because we depend on "live developing" the plugin while using this project to test it inputs.file(new File(rootDir, "Package.swift")) inputs.dir(new File(rootDir, "Sources")) + // If the package description changes, we should execute jextract again, maybe we added jextract to new targets inputs.file(new File(projectDir, "Package.swift")) - inputs.dir(new File(projectDir, "Sources")) - // TODO: we can use package describe --type json to figure out which targets depend on JExtractSwiftPlugin and will produce outputs - // Avoid adding this directory, but create the expected one specifically for all targets - // which WILL produce sources because they have the plugin + // monitor all targets/products which depend on the JExtract plugin + swiftProductsWithJExtractPlugin().each { + logger.info("[swift-java:jextract (Gradle)] Swift input target: ${it}") + inputs.dir(new File(layout.projectDirectory.asFile, "Sources/${it}".toString())) + } outputs.dir(layout.buildDirectory.dir("../.build/plugins/outputs/${layout.projectDirectory.asFile.getName().toLowerCase()}")) File baseSwiftPluginOutputsDir = layout.buildDirectory.dir("../.build/plugins/outputs/").get().asFile @@ -81,7 +101,17 @@ def jextract = tasks.register("jextract", Exec) { workingDir = layout.projectDirectory commandLine "swift" - args("package", "jextract", "-v", "--log-level", "debug") // TODO: pass log level from Gradle build + // FIXME: disable prebuilts until swift-syntax isn't broken on 6.2 anymore: https://github.com/swiftlang/swift-java/issues/418 + args("build", "--disable-experimental-prebuilts") // since Swift targets which need to be jextract-ed have the jextract build plugin, we just need to build + // If we wanted to execute a specific subcommand, we can like this: + // args("run",/* + // "swift-java", "jextract", + // "--swift-module", "MySwiftLibrary", + // // java.package is obtained from the swift-java.config in the swift module + // "--output-java", "${layout.buildDirectory.dir(".build/plugins/outputs/${layout.projectDirectory.asFile.getName().toLowerCase()}/JExtractSwiftPlugin/src/generated/java").get()}", + // "--output-swift", "${layout.buildDirectory.dir(".build/plugins/outputs/${layout.projectDirectory.asFile.getName().toLowerCase()}/JExtractSwiftPlugin/Sources").get()}", + // "--log-level", (logging.level <= LogLevel.INFO ? "debug" : */"info") + // ) } // Add the java-swift generated Java sources @@ -118,8 +148,10 @@ tasks.clean { } dependencies { - implementation(project(':SwiftKit')) + implementation(project(':SwiftKitCore')) + implementation(project(':SwiftKitFFM')) + testRuntimeOnly("org.junit.platform:junit-platform-launcher") // TODO: workaround for not finding junit: https://github.com/gradle/gradle/issues/34512 // TODO: workaround for not finding junit: https://github.com/gradle/gradle/issues/34512 testImplementation(platform("org.junit:junit-bom:5.10.0")) testImplementation("org.junit.jupiter:junit-jupiter") } diff --git a/Samples/SwiftJavaExtractFFMSampleApp/ci-validate.sh b/Samples/SwiftJavaExtractFFMSampleApp/ci-validate.sh new file mode 100755 index 000000000..ff5c32c80 --- /dev/null +++ b/Samples/SwiftJavaExtractFFMSampleApp/ci-validate.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +set -x +set -e + +swift build --disable-experimental-prebuilts # FIXME: until prebuilt swift-syntax isn't broken on 6.2 anymore: https://github.com/swiftlang/swift-java/issues/418 + +./gradlew run +./gradlew test \ No newline at end of file diff --git a/Samples/SwiftJavaExtractFFMSampleApp/gradle.properties b/Samples/SwiftJavaExtractFFMSampleApp/gradle.properties new file mode 120000 index 000000000..7677fb73b --- /dev/null +++ b/Samples/SwiftJavaExtractFFMSampleApp/gradle.properties @@ -0,0 +1 @@ +../gradle.properties \ No newline at end of file diff --git a/Samples/SwiftKitSampleApp/gradlew b/Samples/SwiftJavaExtractFFMSampleApp/gradlew similarity index 100% rename from Samples/SwiftKitSampleApp/gradlew rename to Samples/SwiftJavaExtractFFMSampleApp/gradlew diff --git a/Samples/SwiftKitSampleApp/gradlew.bat b/Samples/SwiftJavaExtractFFMSampleApp/gradlew.bat similarity index 100% rename from Samples/SwiftKitSampleApp/gradlew.bat rename to Samples/SwiftJavaExtractFFMSampleApp/gradlew.bat diff --git a/Samples/SwiftKitSampleApp/src/jmh/java/org/swift/swiftkit/JavaToSwiftBenchmark.java b/Samples/SwiftJavaExtractFFMSampleApp/src/jmh/java/org/swift/swiftkit/ffm/JavaToSwiftBenchmark.java similarity index 92% rename from Samples/SwiftKitSampleApp/src/jmh/java/org/swift/swiftkit/JavaToSwiftBenchmark.java rename to Samples/SwiftJavaExtractFFMSampleApp/src/jmh/java/org/swift/swiftkit/ffm/JavaToSwiftBenchmark.java index ff313fc6d..dffbd0902 100644 --- a/Samples/SwiftKitSampleApp/src/jmh/java/org/swift/swiftkit/JavaToSwiftBenchmark.java +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/jmh/java/org/swift/swiftkit/ffm/JavaToSwiftBenchmark.java @@ -12,7 +12,7 @@ // //===----------------------------------------------------------------------===// -package org.swift.swiftkit; +package org.swift.swiftkit.ffm; import com.example.swift.HelloJava2Swift; import com.example.swift.MySwiftLibrary; @@ -31,12 +31,12 @@ public class JavaToSwiftBenchmark { @State(Scope.Benchmark) public static class BenchmarkState { - ClosableSwiftArena arena; + ClosableAllocatingSwiftArena arena; MySwiftClass obj; @Setup(Level.Trial) public void beforeAll() { - arena = SwiftArena.ofConfined(); + arena = AllocatingSwiftArena.ofConfined(); obj = MySwiftClass.init(1, 2, arena); } diff --git a/Samples/SwiftKitSampleApp/src/jmh/java/org/swift/swiftkit/StringPassingBenchmark.java b/Samples/SwiftJavaExtractFFMSampleApp/src/jmh/java/org/swift/swiftkit/ffm/StringPassingBenchmark.java similarity index 94% rename from Samples/SwiftKitSampleApp/src/jmh/java/org/swift/swiftkit/StringPassingBenchmark.java rename to Samples/SwiftJavaExtractFFMSampleApp/src/jmh/java/org/swift/swiftkit/ffm/StringPassingBenchmark.java index 0e686fb43..258558422 100644 --- a/Samples/SwiftKitSampleApp/src/jmh/java/org/swift/swiftkit/StringPassingBenchmark.java +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/jmh/java/org/swift/swiftkit/ffm/StringPassingBenchmark.java @@ -12,15 +12,14 @@ // //===----------------------------------------------------------------------===// -package org.swift.swiftkit; +package org.swift.swiftkit.ffm; import com.example.swift.HelloJava2Swift; import com.example.swift.MySwiftClass; import com.example.swift.MySwiftLibrary; import org.openjdk.jmh.annotations.*; +import org.swift.swiftkit.core.ClosableSwiftArena; -import java.lang.foreign.Arena; -import java.nio.charset.StandardCharsets; import java.util.concurrent.TimeUnit; @BenchmarkMode(Mode.AverageTime) @@ -40,12 +39,12 @@ public class StringPassingBenchmark { public int stringLen; public String string; - ClosableSwiftArena arena; + ClosableAllocatingSwiftArena arena; MySwiftClass obj; @Setup(Level.Trial) public void beforeAll() { - arena = SwiftArena.ofConfined(); + arena = AllocatingSwiftArena.ofConfined(); obj = MySwiftClass.init(1, 2, arena); string = makeString(stringLen); } diff --git a/Samples/SwiftJavaExtractFFMSampleApp/src/main/java/com/example/swift/HelloJava2Swift.java b/Samples/SwiftJavaExtractFFMSampleApp/src/main/java/com/example/swift/HelloJava2Swift.java new file mode 100644 index 000000000..a13125478 --- /dev/null +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/main/java/com/example/swift/HelloJava2Swift.java @@ -0,0 +1,111 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package com.example.swift; + +// Import swift-extract generated sources + +// Import javakit/swiftkit support libraries + +import org.swift.swiftkit.core.CallTraces; +import org.swift.swiftkit.core.SwiftLibraries; +import org.swift.swiftkit.ffm.AllocatingSwiftArena; +import org.swift.swiftkit.ffm.SwiftRuntime; + +import java.util.Optional; +import java.util.OptionalLong; + +public class HelloJava2Swift { + + public static void main(String[] args) { + boolean traceDowncalls = Boolean.getBoolean("jextract.trace.downcalls"); + System.out.println("Property: jextract.trace.downcalls = " + traceDowncalls); + + System.out.print("Property: java.library.path = " + SwiftLibraries.getJavaLibraryPath()); + + examples(); + } + + static void examples() { + MySwiftLibrary.helloWorld(); + + MySwiftLibrary.globalTakeInt(1337); + + long cnt = MySwiftLibrary.globalWriteString("String from Java"); + + CallTraces.trace("count = " + cnt); + + MySwiftLibrary.globalCallMeRunnable(() -> { + CallTraces.trace("running runnable"); + }); + + CallTraces.trace("getGlobalBuffer().byteSize()=" + MySwiftLibrary.getGlobalBuffer().byteSize()); + + + // Example of using an arena; MyClass.deinit is run at end of scope + try (var arena = AllocatingSwiftArena.ofConfined()) { + MySwiftClass obj = MySwiftClass.init(2222, 7777, arena); + + // just checking retains/releases work + CallTraces.trace("retainCount = " + SwiftRuntime.retainCount(obj)); + SwiftRuntime.retain(obj); + CallTraces.trace("retainCount = " + SwiftRuntime.retainCount(obj)); + SwiftRuntime.release(obj); + CallTraces.trace("retainCount = " + SwiftRuntime.retainCount(obj)); + + obj.setCounter(12); + CallTraces.trace("obj.counter = " + obj.getCounter()); + + obj.voidMethod(); + obj.takeIntMethod(42); + + MySwiftClass otherObj = MySwiftClass.factory(12, 42, arena); + otherObj.voidMethod(); + + MySwiftStruct swiftValue = MySwiftStruct.init(2222, 1111, arena); + CallTraces.trace("swiftValue.capacity = " + swiftValue.getCapacity()); + swiftValue.withCapLen((cap, len) -> { + CallTraces.trace("withCapLenCallback: cap=" + cap + ", len=" + len); + }); + } + + // Example of using 'Data'. + try (var arena = AllocatingSwiftArena.ofConfined()) { + var origBytes = arena.allocateFrom("foobar"); + var origDat = Data.init(origBytes, origBytes.byteSize(), arena); + CallTraces.trace("origDat.count = " + origDat.getCount()); + + var retDat = MySwiftLibrary.globalReceiveReturnData(origDat, arena); + retDat.withUnsafeBytes((retBytes) -> { + var str = retBytes.getString(0); + CallTraces.trace("retStr=" + str); + }); + } + + try (var arena = AllocatingSwiftArena.ofConfined()) { + var bytes = arena.allocateFrom("hello"); + var dat = Data.init(bytes, bytes.byteSize(), arena); + MySwiftLibrary.globalReceiveSomeDataProtocol(dat); + MySwiftLibrary.globalReceiveOptional(OptionalLong.of(12), Optional.of(dat)); + } + + + System.out.println("DONE."); + } + + public static native long jniWriteString(String str); + + public static native long jniGetInt(); + +} diff --git a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/DataImportTest.java b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/DataImportTest.java new file mode 100644 index 000000000..52a63f815 --- /dev/null +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/DataImportTest.java @@ -0,0 +1,49 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package com.example.swift; + +import org.junit.jupiter.api.Test; +import org.swift.swiftkit.ffm.AllocatingSwiftArena; + +import static org.junit.jupiter.api.Assertions.*; + +public class DataImportTest { + @Test + void test_Data_receiveAndReturn() { + try (var arena = AllocatingSwiftArena.ofConfined()) { + var origBytes = arena.allocateFrom("foobar"); + var origDat = Data.init(origBytes, origBytes.byteSize(), arena); + assertEquals(7, origDat.getCount()); + + var retDat = MySwiftLibrary.globalReceiveReturnData(origDat, arena); + assertEquals(7, retDat.getCount()); + retDat.withUnsafeBytes((retBytes) -> { + assertEquals(7, retBytes.byteSize()); + var str = retBytes.getString(0); + assertEquals("foobar", str); + }); + } + } + + @Test + void test_DataProtocol_receive() { + try (var arena = AllocatingSwiftArena.ofConfined()) { + var bytes = arena.allocateFrom("hello"); + var dat = Data.init(bytes, bytes.byteSize(), arena); + var result = MySwiftLibrary.globalReceiveSomeDataProtocol(dat); + assertEquals(6, result); + } + } +} diff --git a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MultipleTypesFromSingleFileTest.java b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MultipleTypesFromSingleFileTest.java new file mode 100644 index 000000000..d3cd791c0 --- /dev/null +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MultipleTypesFromSingleFileTest.java @@ -0,0 +1,36 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package com.example.swift; + +import org.junit.jupiter.api.Test; +import org.swift.swiftkit.core.*; +import org.swift.swiftkit.ffm.*; + +import java.lang.foreign.Arena; +import java.util.Optional; +import java.util.OptionalInt; + +import static org.junit.jupiter.api.Assertions.*; + +public class MultipleTypesFromSingleFileTest { + + @Test + void bothTypesMustHaveBeenGenerated() { + try (var arena = AllocatingSwiftArena.ofConfined()) { + PublicTypeOne.init(arena); + PublicTypeTwo.init(arena); + } + } +} \ No newline at end of file diff --git a/Samples/SwiftKitSampleApp/src/test/java/com/example/swift/MySwiftClassTest.java b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MySwiftClassTest.java similarity index 83% rename from Samples/SwiftKitSampleApp/src/test/java/com/example/swift/MySwiftClassTest.java rename to Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MySwiftClassTest.java index 6c0ceb1e1..1f0464eb6 100644 --- a/Samples/SwiftKitSampleApp/src/test/java/com/example/swift/MySwiftClassTest.java +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MySwiftClassTest.java @@ -16,8 +16,8 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import org.swift.swiftkit.SwiftArena; -import org.swift.swiftkit.SwiftKit; +import org.swift.swiftkit.core.SwiftLibraries; +import org.swift.swiftkit.ffm.AllocatingSwiftArena; import java.io.File; import java.util.stream.Stream; @@ -27,7 +27,7 @@ public class MySwiftClassTest { void checkPaths(Throwable throwable) { - var paths = SwiftKit.getJavaLibraryPath().split(":"); + var paths = SwiftLibraries.getJavaLibraryPath().split(":"); for (var path : paths) { Stream.of(new File(path).listFiles()) .filter(file -> !file.isDirectory()) @@ -41,7 +41,7 @@ void checkPaths(Throwable throwable) { @Test void test_MySwiftClass_voidMethod() { - try(var arena = SwiftArena.ofConfined()) { + try(var arena = AllocatingSwiftArena.ofConfined()) { MySwiftClass o = MySwiftClass.init(12, 42, arena); o.voidMethod(); } catch (Throwable throwable) { @@ -51,7 +51,7 @@ void test_MySwiftClass_voidMethod() { @Test void test_MySwiftClass_makeIntMethod() { - try(var arena = SwiftArena.ofConfined()) { + try(var arena = AllocatingSwiftArena.ofConfined()) { MySwiftClass o = MySwiftClass.init(12, 42, arena); var got = o.makeIntMethod(); assertEquals(12, got); @@ -61,7 +61,7 @@ void test_MySwiftClass_makeIntMethod() { @Test @Disabled // TODO: Need var mangled names in interfaces void test_MySwiftClass_property_len() { - try(var arena = SwiftArena.ofConfined()) { + try(var arena = AllocatingSwiftArena.ofConfined()) { MySwiftClass o = MySwiftClass.init(12, 42, arena); var got = o.getLen(); assertEquals(12, got); diff --git a/Samples/SwiftKitSampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java similarity index 92% rename from Samples/SwiftKitSampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java rename to Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java index 2df53843c..a6c34b579 100644 --- a/Samples/SwiftKitSampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java @@ -14,15 +14,10 @@ package com.example.swift; -import com.example.swift.MySwiftLibrary; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import org.swift.swiftkit.SwiftKit; -import java.util.Arrays; import java.util.concurrent.CountDownLatch; -import java.util.stream.Collectors; import static org.junit.jupiter.api.Assertions.*; diff --git a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/OptionalImportTest.java b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/OptionalImportTest.java new file mode 100644 index 000000000..57e8dba61 --- /dev/null +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/OptionalImportTest.java @@ -0,0 +1,35 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package com.example.swift; + +import org.junit.jupiter.api.Test; +import org.swift.swiftkit.ffm.AllocatingSwiftArena; + +import java.util.Optional; +import java.util.OptionalLong; + +import static org.junit.jupiter.api.Assertions.*; + +public class OptionalImportTest { + @Test + void test_Optional_receive() { + try (var arena = AllocatingSwiftArena.ofConfined()) { + var origBytes = arena.allocateFrom("foobar"); + var data = Data.init(origBytes, origBytes.byteSize(), arena); + assertEquals(0, MySwiftLibrary.globalReceiveOptional(OptionalLong.empty(), Optional.empty())); + assertEquals(3, MySwiftLibrary.globalReceiveOptional(OptionalLong.of(12), Optional.of(data))); + } + } +} diff --git a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/UnsignedNumbersTest.java b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/UnsignedNumbersTest.java new file mode 100644 index 000000000..beb0f817f --- /dev/null +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/UnsignedNumbersTest.java @@ -0,0 +1,36 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package com.example.swift; + +import org.junit.jupiter.api.Test; +import org.swift.swiftkit.ffm.AllocatingSwiftArena; + +public class UnsignedNumbersTest { + @Test + void take_uint32() { + try (var arena = AllocatingSwiftArena.ofConfined()) { + var c = MySwiftClass.init(1, 2, arena); + c.takeUnsignedInt(128); + } + } + + @Test + void take_uint64() { + try (var arena = AllocatingSwiftArena.ofConfined()) { + var c = MySwiftClass.init(1, 2, arena); + c.takeUnsignedLong(Long.MAX_VALUE); + } + } +} diff --git a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/WithBufferTest.java b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/WithBufferTest.java new file mode 100644 index 000000000..54206423c --- /dev/null +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/WithBufferTest.java @@ -0,0 +1,60 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package com.example.swift; + +import org.junit.jupiter.api.Test; +import org.swift.swiftkit.core.*; +import org.swift.swiftkit.ffm.*; + +import static org.junit.jupiter.api.Assertions.*; + +import java.lang.foreign.ValueLayout; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.IntStream; + +public class WithBufferTest { + @Test + void test_withBuffer() { + AtomicLong bufferSize = new AtomicLong(); + MySwiftLibrary.withBuffer((buf) -> { + CallTraces.trace("withBuffer{$0.byteSize()}=" + buf.byteSize()); + bufferSize.set(buf.byteSize()); + }); + + assertEquals(124, bufferSize.get()); + } + + @Test + void test_sumAllByteArrayElements_throughMemorySegment() { + byte[] bytes = new byte[124]; + Arrays.fill(bytes, (byte) 1); + + try (var arena = AllocatingSwiftArena.ofConfined()) { + // NOTE: We cannot use MemorySegment.ofArray because that creates a HEAP backed segment and therefore cannot pass into native: + // java.lang.IllegalArgumentException: Heap segment not allowed: MemorySegment{ kind: heap, heapBase: [B@5b6ec132, address: 0x0, byteSize: 124 } + // MemorySegment bytesSegment = MemorySegment.ofArray(bytes); // NO COPY (!) + // MySwiftLibrary.sumAllByteArrayElements(bytesSegment, bytes.length); + + var bytesCopy = arena.allocateFrom(ValueLayout.JAVA_BYTE, bytes); + var swiftSideSum = MySwiftLibrary.sumAllByteArrayElements(bytesCopy, bytes.length); + + System.out.println("swiftSideSum = " + swiftSideSum); + + int javaSideSum = IntStream.range(0, bytes.length).map(i -> bytes[i]).sum(); + assertEquals(javaSideSum, swiftSideSum); + } + } +} diff --git a/Samples/SwiftKitSampleApp/src/test/java/org/swift/swiftkit/MySwiftClassTest.java b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/org/swift/swiftkitffm/MySwiftClassTest.java similarity index 66% rename from Samples/SwiftKitSampleApp/src/test/java/org/swift/swiftkit/MySwiftClassTest.java rename to Samples/SwiftJavaExtractFFMSampleApp/src/test/java/org/swift/swiftkitffm/MySwiftClassTest.java index 78da5a642..17db4c418 100644 --- a/Samples/SwiftKitSampleApp/src/test/java/org/swift/swiftkit/MySwiftClassTest.java +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/org/swift/swiftkitffm/MySwiftClassTest.java @@ -12,28 +12,30 @@ // //===----------------------------------------------------------------------===// -package org.swift.swiftkit; +package org.swift.swiftkitffm; import static org.junit.jupiter.api.Assertions.assertEquals; import org.junit.jupiter.api.Test; import com.example.swift.MySwiftClass; +import org.swift.swiftkit.ffm.AllocatingSwiftArena; +import org.swift.swiftkit.ffm.SwiftRuntime; public class MySwiftClassTest { @Test void call_retain_retainCount_release() { - var arena = SwiftArena.ofConfined(); + var arena = AllocatingSwiftArena.ofConfined(); var obj = MySwiftClass.init(1, 2, arena); - assertEquals(1, SwiftKit.retainCount(obj)); + assertEquals(1, SwiftRuntime.retainCount(obj)); // TODO: test directly on SwiftHeapObject inheriting obj - SwiftKit.retain(obj); - assertEquals(2, SwiftKit.retainCount(obj)); + SwiftRuntime.retain(obj); + assertEquals(2, SwiftRuntime.retainCount(obj)); - SwiftKit.release(obj); - assertEquals(1, SwiftKit.retainCount(obj)); + SwiftRuntime.release(obj); + assertEquals(1, SwiftRuntime.retainCount(obj)); } } diff --git a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/org/swift/swiftkitffm/MySwiftStructTest.java b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/org/swift/swiftkitffm/MySwiftStructTest.java new file mode 100644 index 000000000..d904f7e82 --- /dev/null +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/org/swift/swiftkitffm/MySwiftStructTest.java @@ -0,0 +1,58 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package org.swift.swiftkitffm; + +import com.example.swift.MySwiftStruct; +import org.junit.jupiter.api.Test; +import org.swift.swiftkit.ffm.AllocatingSwiftArena; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class MySwiftStructTest { + + @Test + void create_struct() { + try (var arena = AllocatingSwiftArena.ofConfined()) { + long cap = 12; + long len = 34; + var struct = MySwiftStruct.init(cap, len, arena); + + assertEquals(cap, struct.getCapacity()); + assertEquals(len, struct.getLength()); + } + } + + @Test + void testSubscript() { + try (var arena = AllocatingSwiftArena.ofConfined()) { + MySwiftStruct s = MySwiftStruct.init(1337, 42, arena); + long currentValue = s.getSubscript(); + s.setSubscript(66); + assertEquals(0, currentValue); + assertEquals(66, s.getSubscriptValue()); + } + } + + @Test + void testSubscriptWithParams() { + try (var arena = AllocatingSwiftArena.ofConfined()) { + MySwiftStruct s = MySwiftStruct.init(1337, 42, arena); + long currentValue = s.getSubscript(1); + s.setSubscript(1, 66); + assertEquals(20, currentValue); + assertEquals(66, s.getSubscriptArrayValue(1)); + } + } +} diff --git a/Samples/SwiftKitSampleApp/src/test/java/org/swift/swiftkit/SwiftArenaTest.java b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/org/swift/swiftkitffm/SwiftArenaTest.java similarity index 82% rename from Samples/SwiftKitSampleApp/src/test/java/org/swift/swiftkit/SwiftArenaTest.java rename to Samples/SwiftJavaExtractFFMSampleApp/src/test/java/org/swift/swiftkitffm/SwiftArenaTest.java index 0d900a623..08dd9b1bc 100644 --- a/Samples/SwiftKitSampleApp/src/test/java/org/swift/swiftkit/SwiftArenaTest.java +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/org/swift/swiftkitffm/SwiftArenaTest.java @@ -12,21 +12,18 @@ // //===----------------------------------------------------------------------===// -package org.swift.swiftkit; +package org.swift.swiftkitffm; import com.example.swift.MySwiftClass; import com.example.swift.MySwiftStruct; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledIf; -import org.swift.swiftkit.util.PlatformUtils; - -import java.util.Arrays; -import java.util.stream.Collectors; +import org.swift.swiftkit.core.util.PlatformUtils; +import org.swift.swiftkit.ffm.AllocatingSwiftArena; +import org.swift.swiftkit.ffm.SwiftRuntime; import static org.junit.jupiter.api.Assertions.*; -import static org.swift.swiftkit.SwiftKit.*; -import static org.swift.swiftkit.SwiftKit.retainCount; +import static org.swift.swiftkit.ffm.SwiftRuntime.retainCount; public class SwiftArenaTest { @@ -39,13 +36,13 @@ static boolean isAmd64() { @Test @DisabledIf("isAmd64") public void arena_releaseClassOnClose_class_ok() { - try (var arena = SwiftArena.ofConfined()) { + try (var arena = AllocatingSwiftArena.ofConfined()) { var obj = MySwiftClass.init(1, 2, arena); - retain(obj); + SwiftRuntime.retain(obj); assertEquals(2, retainCount(obj)); - release(obj); + SwiftRuntime.release(obj); assertEquals(1, retainCount(obj)); } } @@ -56,14 +53,14 @@ public void arena_releaseClassOnClose_class_ok() { public void arena_markAsDestroyed_preventUseAfterFree_class() { MySwiftClass unsafelyEscapedOutsideArenaScope = null; - try (var arena = SwiftArena.ofConfined()) { + try (var arena = AllocatingSwiftArena.ofConfined()) { var obj = MySwiftClass.init(1, 2, arena); unsafelyEscapedOutsideArenaScope = obj; } try { unsafelyEscapedOutsideArenaScope.echoIntMethod(1); - fail("Expected exception to be thrown! Object was suposed to be dead."); + fail("Expected exception to be thrown! Object was supposed to be dead."); } catch (IllegalStateException ex) { return; } @@ -75,14 +72,14 @@ public void arena_markAsDestroyed_preventUseAfterFree_class() { public void arena_markAsDestroyed_preventUseAfterFree_struct() { MySwiftStruct unsafelyEscapedOutsideArenaScope = null; - try (var arena = SwiftArena.ofConfined()) { + try (var arena = AllocatingSwiftArena.ofConfined()) { var s = MySwiftStruct.init(1, 2, arena); unsafelyEscapedOutsideArenaScope = s; } try { unsafelyEscapedOutsideArenaScope.echoIntMethod(1); - fail("Expected exception to be thrown! Object was suposed to be dead."); + fail("Expected exception to be thrown! Object was supposed to be dead."); } catch (IllegalStateException ex) { return; } diff --git a/Samples/SwiftJavaExtractJNISampleApp/Package.swift b/Samples/SwiftJavaExtractJNISampleApp/Package.swift new file mode 100644 index 000000000..6343e0dbd --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/Package.swift @@ -0,0 +1,79 @@ +// swift-tools-version: 6.1 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import CompilerPluginSupport +import PackageDescription + +import class Foundation.FileManager +import class Foundation.ProcessInfo + +// Note: the JAVA_HOME environment variable must be set to point to where +// Java is installed, e.g., +// Library/Java/JavaVirtualMachines/openjdk-21.jdk/Contents/Home. +func findJavaHome() -> String { + if let home = ProcessInfo.processInfo.environment["JAVA_HOME"] { + return home + } + + // This is a workaround for envs (some IDEs) which have trouble with + // picking up env variables during the build process + let path = "\(FileManager.default.homeDirectoryForCurrentUser.path()).java_home" + if let home = try? String(contentsOfFile: path, encoding: .utf8) { + if let lastChar = home.last, lastChar.isNewline { + return String(home.dropLast()) + } + + return home + } + + fatalError("Please set the JAVA_HOME environment variable to point to where Java is installed.") +} +let javaHome = findJavaHome() + +let javaIncludePath = "\(javaHome)/include" +#if os(Linux) + let javaPlatformIncludePath = "\(javaIncludePath)/linux" +#elseif os(macOS) + let javaPlatformIncludePath = "\(javaIncludePath)/darwin" +#else + // TODO: Handle windows as well + #error("Currently only macOS and Linux platforms are supported, this may change in the future.") +#endif + +let package = Package( + name: "JExtractJNISampleApp", + platforms: [ + .macOS(.v15) + ], + products: [ + .library( + name: "MySwiftLibrary", + type: .dynamic, + targets: ["MySwiftLibrary"] + ) + + ], + dependencies: [ + .package(name: "swift-java", path: "../../") + ], + targets: [ + .target( + name: "MySwiftLibrary", + dependencies: [ + .product(name: "SwiftJava", package: "swift-java"), + .product(name: "CSwiftJavaJNI", package: "swift-java"), + .product(name: "SwiftJavaRuntimeSupport", package: "swift-java"), + ], + exclude: [ + "swift-java.config" + ], + swiftSettings: [ + .swiftLanguageMode(.v5), + .unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"]), + ], + plugins: [ + .plugin(name: "JExtractSwiftPlugin", package: "swift-java") + ] + ) + ] +) diff --git a/Sources/JavaKitConfigurationShared/GenerationMode.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Alignment.swift similarity index 84% rename from Sources/JavaKitConfigurationShared/GenerationMode.swift rename to Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Alignment.swift index ff1d081da..760c564b9 100644 --- a/Sources/JavaKitConfigurationShared/GenerationMode.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Alignment.swift @@ -12,7 +12,7 @@ // //===----------------------------------------------------------------------===// -public enum GenerationMode: String, Codable { - /// Foreign Value and Memory API - case ffm +public enum Alignment: String { + case horizontal + case vertical } diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Arrays.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Arrays.swift new file mode 100644 index 000000000..b6f050c12 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Arrays.swift @@ -0,0 +1,60 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftJava + +public func booleanArray(array: [Bool]) -> [Bool] { + return array +} + +public func byteArray(array: [UInt8]) -> [UInt8] { + return array +} + +public func byteArrayExplicit(array: Array) -> Array { + return array +} + +public func charArray(array: [UInt16]) -> [UInt16] { + return array +} + +public func shortArray(array: [Int16]) -> [Int16] { + return array +} + +public func intArray(array: [Int32]) -> [Int32] { + return array +} + +public func longArray(array: [Int64]) -> [Int64] { + return array +} + +public func floatArray(array: [Float]) -> [Float] { + return array +} + +public func doubleArray(array: [Double]) -> [Double] { + return array +} + +public func stringArray(array: [String]) -> [String] { + return array +} + +public func objectArray(array: [MySwiftClass]) -> [MySwiftClass] { + return array +} + diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Async.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Async.swift new file mode 100644 index 000000000..99f3a3933 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Async.swift @@ -0,0 +1,42 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftJava + +public func asyncSum(i1: Int64, i2: Int64) async -> Int64 { + return i1 + i2 +} + +public func asyncSleep() async throws { + try await Task.sleep(for: .milliseconds(500)) +} + +public func asyncCopy(myClass: MySwiftClass) async throws -> MySwiftClass { + let new = MySwiftClass(x: myClass.x, y: myClass.y) + try await Task.sleep(for: .milliseconds(500)) + return new +} + +public func asyncOptional(i: Int64) async throws -> Int64? { + try await Task.sleep(for: .milliseconds(100)) + return i +} + +public func asyncThrows() async throws { + throw MySwiftError.swiftError +} + +public func asyncString(input: String) async -> String { + return input +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CallbackProtcol.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CallbackProtcol.swift new file mode 100644 index 000000000..985771bef --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CallbackProtcol.swift @@ -0,0 +1,77 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftJava + +public protocol CallbackProtocol { + func withBool(_ input: Bool) -> Bool + func withInt8(_ input: Int8) -> Int8 + func withUInt16(_ input: UInt16) -> UInt16 + func withInt16(_ input: Int16) -> Int16 + func withInt32(_ input: Int32) -> Int32 + func withInt64(_ input: Int64) -> Int64 + func withFloat(_ input: Float) -> Float + func withDouble(_ input: Double) -> Double + func withString(_ input: String) -> String + func withVoid() + func withObject(_ input: MySwiftClass) -> MySwiftClass + func withOptionalInt64(_ input: Int64?) -> Int64? + func withOptionalObject(_ input: MySwiftClass?) -> Optional +} + +public struct CallbackOutput { + public let bool: Bool + public let int8: Int8 + public let uint16: UInt16 + public let int16: Int16 + public let int32: Int32 + public let int64: Int64 + public let _float: Float + public let _double: Double + public let string: String + public let object: MySwiftClass + public let optionalInt64: Int64? + public let optionalObject: MySwiftClass? +} + +public func outputCallbacks( + _ callbacks: some CallbackProtocol, + bool: Bool, + int8: Int8, + uint16: UInt16, + int16: Int16, + int32: Int32, + int64: Int64, + _float: Float, + _double: Double, + string: String, + object: MySwiftClass, + optionalInt64: Int64?, + optionalObject: MySwiftClass? +) -> CallbackOutput { + return CallbackOutput( + bool: callbacks.withBool(bool), + int8: callbacks.withInt8(int8), + uint16: callbacks.withUInt16(uint16), + int16: callbacks.withInt16(int16), + int32: callbacks.withInt32(int32), + int64: callbacks.withInt64(int64), + _float: callbacks.withFloat(_float), + _double: callbacks.withDouble(_double), + string: callbacks.withString(string), + object: callbacks.withObject(object), + optionalInt64: callbacks.withOptionalInt64(optionalInt64), + optionalObject: callbacks.withOptionalObject(optionalObject) + ) +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Closures.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Closures.swift new file mode 100644 index 000000000..00d1f4b0f --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Closures.swift @@ -0,0 +1,31 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +public func emptyClosure(closure: () -> ()) { + closure() +} + +public func closureWithInt(input: Int64, closure: (Int64) -> Int64) -> Int64 { + return closure(input) +} + +public func closureMultipleArguments( + input1: Int64, + input2: Int64, + closure: (Int64, Int64) -> Int64 +) -> Int64 { + return closure(input1, input2) +} + + diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ConcreteProtocolAB.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ConcreteProtocolAB.swift new file mode 100644 index 000000000..d83fbb28d --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ConcreteProtocolAB.swift @@ -0,0 +1,32 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +public class ConcreteProtocolAB: ProtocolA, ProtocolB { + public let constantA: Int64 + public let constantB: Int64 + public var mutable: Int64 = 0 + + public func name() -> String { + return "ConcreteProtocolAB" + } + + public func makeClass() -> MySwiftClass { + return MySwiftClass(x: 10, y: 50) + } + + public init(constantA: Int64, constantB: Int64) { + self.constantA = constantA + self.constantB = constantB + } +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MultiplePublicTypes.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MultiplePublicTypes.swift new file mode 100644 index 000000000..b7a02d787 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MultiplePublicTypes.swift @@ -0,0 +1,26 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// This file exists to exercise the swiftpm plugin generating separate output Java files +// for the public types; because Java public types must be in a file with the same name as the type. + +public struct PublicTypeOne { + public init() {} + public func test() {} +} + +public struct PublicTypeTwo { + public init() {} + public func test() {} +} \ No newline at end of file diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift new file mode 100644 index 000000000..72f2fa357 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift @@ -0,0 +1,113 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftJava + +public class MySwiftClass { + public let x: Int64 + public let y: Int64 + + public let byte: UInt8 = 0 + public let constant: Int64 = 100 + public var mutable: Int64 = 0 + public var product: Int64 { + return x * y + } + public var throwingVariable: Int64 { + get throws { + throw MySwiftClassError.swiftError + } + } + public var mutableDividedByTwo: Int64 { + get { + return mutable / 2 + } + set { + mutable = newValue * 2 + } + } + public let warm: Bool = false + public var getAsync: Int64 { + get async { + return 42 + } + } + + public static func method() { + p("Hello from static method in a class!") + } + + public init(x: Int64, y: Int64) { + self.x = x + self.y = y + p("\(self)") + } + + public init() { + self.x = 10 + self.y = 5 + } + + convenience public init(throwing: Bool) throws { + if throwing { + throw MySwiftError.swiftError + } else { + self.init() + } + } + + deinit { + p("deinit called!") + } + + public func sum() -> Int64 { + return x + y + } + + public func xMultiplied(by z: Int64) -> Int64 { + return x * z; + } + + enum MySwiftClassError: Error { + case swiftError + } + + public func throwingFunction() throws { + throw MySwiftClassError.swiftError + } + + public func sumX(with other: MySwiftClass) -> Int64 { + return self.x + other.x + } + + public func copy() -> MySwiftClass { + return MySwiftClass(x: self.x, y: self.y) + } + + public func addXWithJavaLong(_ other: JavaLong) -> Int64 { + return self.x + other.longValue() + } +} + +extension MySwiftClass: CustomStringConvertible { + public var description: String { + "MySwiftClass(x: \(x), y: \(y))" + } +} + +extension MySwiftClass: CustomDebugStringConvertible { + public var debugDescription: String { + "debug: MySwiftClass(x: \(x), y: \(y))" + } +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftError.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftError.swift new file mode 100644 index 000000000..d9d77d38b --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftError.swift @@ -0,0 +1,17 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +enum MySwiftError: Error { + case swiftError +} diff --git a/Samples/SwiftKitSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift similarity index 56% rename from Samples/SwiftKitSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift rename to Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift index 2bd1905c4..e278326e0 100644 --- a/Samples/SwiftKitSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -18,33 +18,38 @@ // No annotations are necessary on the Swift side to perform the export. #if os(Linux) -import Glibc + import Glibc +#elseif os(Android) + import Android #else -import Darwin.C + import Darwin.C #endif +public var globalVariable: Int64 = 0 + public func helloWorld() { p("\(#function)") } -public func globalTakeInt(i: Int) { +public func globalTakeInt(i: Int64) { p("i:\(i)") } -public func globalMakeInt() -> Int { +public func globalMakeInt() -> Int64 { return 42 } -public func globalWriteString(string: String) -> Int { - return string.count +public func globalWriteString(string: String) -> Int64 { + return Int64(string.count) } -public func globalTakeIntInt(i: Int, j: Int) { +public func globalTakeIntInt(i: Int64, j: Int64) { p("i:\(i), j:\(j)") } -public func globalCallMeRunnable(run: () -> ()) { - run() +public func echoUnsignedInt(i: UInt32, j: UInt64) -> UInt64 { + p("i:\(i), j:\(j)") + return UInt64(i) + j } // ==== Internal helpers @@ -54,11 +59,11 @@ func p(_ msg: String, file: String = #fileID, line: UInt = #line, function: Stri fflush(stdout) } - #if os(Linux) - // FIXME: why do we need this workaround? - @_silgen_name("_objc_autoreleaseReturnValue") - public func _objc_autoreleaseReturnValue(a: Any) {} +#if os(Linux) + // FIXME: why do we need this workaround? + @_silgen_name("_objc_autoreleaseReturnValue") + public func _objc_autoreleaseReturnValue(a: Any) {} - @_silgen_name("objc_autoreleaseReturnValue") - public func objc_autoreleaseReturnValue(a: Any) {} - #endif + @_silgen_name("objc_autoreleaseReturnValue") + public func objc_autoreleaseReturnValue(a: Any) {} +#endif diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift new file mode 100644 index 000000000..34686f410 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift @@ -0,0 +1,63 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +public struct MySwiftStruct { + private var cap: Int64 + public var len: Int64 + private var subscriptValue: Int64 + private var subscriptArray: [Int64] + + public init(cap: Int64, len: Int64) { + self.cap = cap + self.len = len + self.subscriptValue = 0 + self.subscriptArray = [10, 20, 15, 75] + } + + public init?(doInit: Bool) { + if doInit { + self.init(cap: 10, len: 10) + } else { + return nil + } + } + + public func getCapacity() -> Int64 { + self.cap + } + + public mutating func increaseCap(by value: Int64) -> Int64 { + precondition(value > 0) + self.cap += value + return self.cap + } + + public func getSubscriptValue() -> Int64 { + return self.subscriptValue + } + + public func getSubscriptArrayValue(index: Int64) -> Int64 { + return self.subscriptArray[Int(index)] + } + + public subscript() -> Int64 { + get { return subscriptValue } + set { subscriptValue = newValue } + } + + public subscript(index: Int64) -> Int64 { + get { return subscriptArray[Int(index)] } + set { subscriptArray[Int(index)] = newValue } + } +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/NestedTypes.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/NestedTypes.swift new file mode 100644 index 000000000..fb2b49248 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/NestedTypes.swift @@ -0,0 +1,47 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftJava + +public class A { + public init() {} + + public class B { + public init() {} + + public struct C { + public init() {} + + public func g(a: A, b: B, bbc: BB.C) {} + } + } + + public class BB { + public init() {} + + public struct C { + public init() {} + } + } + + public func f(a: A, b: A.B, c: A.B.C, bb: BB, bbc: BB.C) {} +} + +public enum NestedEnum { + case one(OneStruct) + + public struct OneStruct { + public init() {} + } +} \ No newline at end of file diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Optionals.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Optionals.swift new file mode 100644 index 000000000..ca1d74586 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Optionals.swift @@ -0,0 +1,79 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftJava + +public func optionalBool(input: Optional) -> Bool? { + return input +} + +public func optionalByte(input: Optional) -> Int8? { + return input +} + +public func optionalChar(input: Optional) -> UInt16? { + return input +} + +public func optionalShort(input: Optional) -> Int16? { + return input +} + +public func optionalInt(input: Optional) -> Int32? { + return input +} + +public func optionalLong(input: Optional) -> Int64? { + return input +} + +public func optionalFloat(input: Optional) -> Float? { + return input +} + +public func optionalDouble(input: Optional) -> Double? { + return input +} + +public func optionalString(input: Optional) -> String? { + return input +} + +public func optionalClass(input: Optional) -> MySwiftClass? { + return input +} + +public func optionalJavaKitLong(input: Optional) -> Int64? { + if let input { + return input.longValue() + } else { + return nil + } +} + +public func optionalThrowing() throws -> Int64? { + throw MySwiftError.swiftError +} + +public func multipleOptionals( + input1: Optional, + input2: Optional, + input3: Optional, + input4: Optional, + input5: Optional, + input6: Optional, + input7: Optional +) -> Int64? { + return 1 +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolA.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolA.swift new file mode 100644 index 000000000..6e19596f9 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolA.swift @@ -0,0 +1,25 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +public protocol ProtocolA { + var constantA: Int64 { get } + var mutable: Int64 { get set } + + func name() -> String + func makeClass() -> MySwiftClass +} + +public func takeProtocol(_ proto1: any ProtocolA, _ proto2: some ProtocolA) -> Int64 { + return proto1.constantA + proto2.constantA +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolB.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolB.swift new file mode 100644 index 000000000..70d075c25 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolB.swift @@ -0,0 +1,29 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +public protocol ProtocolB { + var constantB: Int64 { get } +} + +public func takeCombinedProtocol(_ proto: some ProtocolA & ProtocolB) -> Int64 { + return proto.constantA + proto.constantB +} + +public func takeGenericProtocol(_ proto1: First, _ proto2: Second) -> Int64 { + return proto1.constantA + proto2.constantB +} + +public func takeCombinedGenericProtocol(_ proto: T) -> Int64 { + return proto.constantA + proto.constantB +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Storage.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Storage.swift new file mode 100644 index 000000000..488a78cc1 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Storage.swift @@ -0,0 +1,36 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftJava + +public class StorageItem { + public let value: Int64 + + public init(value: Int64) { + self.value = value + } +} + +public protocol Storage { + func load() -> StorageItem + func save(_ item: StorageItem) +} + +public func saveWithStorage(_ item: StorageItem, s: any Storage) { + s.save(item); +} + +public func loadWithStorage(s: any Storage) -> StorageItem { + return s.load(); +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Vehicle.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Vehicle.swift new file mode 100644 index 000000000..9d9155add --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Vehicle.swift @@ -0,0 +1,63 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +public enum Vehicle { + case bicycle + case car(String, trailer: String?) + case motorbike(String, horsePower: Int64, helmets: Int32?) + indirect case transformer(front: Vehicle, back: Vehicle) + case boat(passengers: Int32?, length: Int16?) + + public init?(name: String) { + switch name { + case "bicycle": self = .bicycle + case "car": self = .car("Unknown", trailer: nil) + case "motorbike": self = .motorbike("Unknown", horsePower: 0, helmets: nil) + case "boat": self = .boat(passengers: nil, length: nil) + default: return nil + } + } + + public var name: String { + switch self { + case .bicycle: "bicycle" + case .car: "car" + case .motorbike: "motorbike" + case .transformer: "transformer" + case .boat: "boat" + } + } + + public func isFasterThan(other: Vehicle) -> Bool { + switch (self, other) { + case (.bicycle, .bicycle), (.bicycle, .car), (.bicycle, .motorbike), (.bicycle, .transformer): false + case (.car, .bicycle): true + case (.car, .motorbike), (.car, .transformer), (.car, .car): false + case (.motorbike, .bicycle), (.motorbike, .car): true + case (.motorbike, .motorbike), (.motorbike, .transformer): false + case (.transformer, .bicycle), (.transformer, .car), (.transformer, .motorbike): true + case (.transformer, .transformer): false + default: false + } + } + + public mutating func upgrade() { + switch self { + case .bicycle: self = .car("Unknown", trailer: nil) + case .car: self = .motorbike("Unknown", horsePower: 0, helmets: nil) + case .motorbike: self = .transformer(front: .car("BMW", trailer: nil), back: self) + case .transformer, .boat: break + } + } +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/swift-java.config b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/swift-java.config new file mode 100644 index 000000000..52143b7f7 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/swift-java.config @@ -0,0 +1,6 @@ +{ + "javaPackage": "com.example.swift", + "mode": "jni", + "enableJavaCallbacks": true, + "logLevel": "debug" +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/build.gradle b/Samples/SwiftJavaExtractJNISampleApp/build.gradle new file mode 100644 index 000000000..7bb64c554 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/build.gradle @@ -0,0 +1,219 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import groovy.json.JsonSlurper +import org.swift.swiftkit.gradle.BuildUtils + +import java.nio.file.* +import kotlinx.serialization.json.* + +plugins { + id("build-logic.java-application-conventions") + id("me.champeau.jmh") version "0.7.2" +} + +group = "org.swift.swiftkit" +version = "1.0-SNAPSHOT" + +repositories { + mavenCentral() +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(25)) + } +} + +def swiftProductsWithJExtractPlugin() { + def stdout = new ByteArrayOutputStream() + def stderr = new ByteArrayOutputStream() + + def processBuilder = new ProcessBuilder('swift', 'package', 'describe', '--type', 'json') + def process = processBuilder.start() + + process.consumeProcessOutput(stdout, stderr) + process.waitFor() + + def exitValue = process.exitValue() + def jsonOutput = stdout.toString() + + if (exitValue == 0) { + def json = new JsonSlurper().parseText(jsonOutput) + def products = json.targets + .findAll { target -> + target.product_dependencies?.contains("JExtractSwiftPlugin") + } + .collectMany { target -> + target.product_memberships ?: [] + } + return products + } else { + logger.warn("Command failed: ${stderr.toString()}") + return [] + } +} + + +def swiftCheckValid = tasks.register("swift-check-valid", Exec) { + commandLine "swift" + args("-version") +} + +def jextract = tasks.register("jextract", Exec) { + description = "Generate Java wrappers for swift target" + dependsOn swiftCheckValid + + // only because we depend on "live developing" the plugin while using this project to test it + inputs.file(new File(rootDir, "Package.swift")) + inputs.dir(new File(rootDir, "Sources")) + + // If the package description changes, we should execute jextract again, maybe we added jextract to new targets + inputs.file(new File(projectDir, "Package.swift")) + + // monitor all targets/products which depend on the JExtract plugin + swiftProductsWithJExtractPlugin().each { + logger.info("[swift-java:jextract (Gradle)] Swift input target: ${it}") + inputs.dir(new File(layout.projectDirectory.asFile, "Sources/${it}".toString())) + } + outputs.dir(layout.buildDirectory.dir("../.build/plugins/outputs/${layout.projectDirectory.asFile.getName().toLowerCase()}")) + + File baseSwiftPluginOutputsDir = layout.buildDirectory.dir("../.build/plugins/outputs/").get().asFile + if (!baseSwiftPluginOutputsDir.exists()) { + baseSwiftPluginOutputsDir.mkdirs() + } + Files.walk(layout.buildDirectory.dir("../.build/plugins/outputs/").get().asFile.toPath()).each { + // Add any Java sources generated by the plugin to our sourceSet + if (it.endsWith("JExtractSwiftPlugin/src/generated/java")) { + outputs.dir(it) + } + } + + // FIXME: disable prebuilts until swift-syntax isn't broken on 6.2 anymore: https://github.com/swiftlang/swift-java/issues/418 + def cmdArgs = ["build", "--disable-experimental-prebuilts", "--disable-sandbox"] + + // Check if the 'swiftSdk' project property was passed + if (project.hasProperty('swiftSdk')) { + // If it was, add the --sdk argument and its value + cmdArgs.add("--swift-sdk") + cmdArgs.add(project.property('swiftSdk').toString()) + } + + workingDir = layout.projectDirectory + commandLine "swift" + args(cmdArgs) // since Swift targets which need to be jextract-ed have the jextract build plugin, we just need to build + // If we wanted to execute a specific subcommand, we can like this: + // args("run",/* + // "swift-java", "jextract", + // "--swift-module", "MySwiftLibrary", + // // java.package is obtained from the swift-java.config in the swift module + // "--output-java", "${layout.buildDirectory.dir(".build/plugins/outputs/${layout.projectDirectory.asFile.getName().toLowerCase()}/JExtractSwiftPlugin/src/generated/java").get()}", + // "--output-swift", "${layout.buildDirectory.dir(".build/plugins/outputs/${layout.projectDirectory.asFile.getName().toLowerCase()}/JExtractSwiftPlugin/Sources").get()}", + // "--log-level", (logging.level <= LogLevel.INFO ? "debug" : */"info") + // ) +} + +// Add the java-swift generated Java sources +sourceSets { + main { + java { + srcDir(jextract) + } + } + test { + java { + srcDir(jextract) + } + } + jmh { + java { + srcDir(jextract) + } + } +} + +tasks.build { + dependsOn("jextract") +} + + +def cleanSwift = tasks.register("cleanSwift", Exec) { + workingDir = layout.projectDirectory + commandLine "swift" + args("package", "clean") +} +tasks.clean { + dependsOn("cleanSwift") +} + +dependencies { + implementation(project(':SwiftKitCore')) + + testRuntimeOnly("org.junit.platform:junit-platform-launcher") // TODO: workaround for not finding junit: https://github.com/gradle/gradle/issues/34512 + testImplementation(platform("org.junit:junit-bom:5.10.0")) + testImplementation("org.junit.jupiter:junit-jupiter") +} + +tasks.named('test', Test) { + useJUnitPlatform() + + testLogging { + events "failed" + exceptionFormat "full" + } +} + +application { + mainClass = "com.example.swift.HelloJava2SwiftJNI" + + applicationDefaultJvmArgs = [ + "--enable-native-access=ALL-UNNAMED", + + // Include the library paths where our dylibs are that we want to load and call + "-Djava.library.path=" + + (BuildUtils.javaLibraryPaths(rootDir) + + BuildUtils.javaLibraryPaths(project.projectDir)).join(":"), + + + // Enable tracing downcalls (to Swift) + "-Djextract.trace.downcalls=true" + ] +} + +String jmhIncludes = findProperty("jmhIncludes") + +jmh { + if (jmhIncludes != null) { + includes = [jmhIncludes] + } + + jvmArgsAppend = [ + "--enable-native-access=ALL-UNNAMED", + + "-Djava.library.path=" + + (BuildUtils.javaLibraryPaths(rootDir) + + BuildUtils.javaLibraryPaths(project.projectDir)).join(":"), + + // Enable tracing downcalls (to Swift) + "-Djextract.trace.downcalls=false" + ] +} + +task printGradleHome { + doLast { + println "Gradle Home: ${gradle.gradleHomeDir}" + println "Gradle Version: ${gradle.gradleVersion}" + println "Gradle User Home: ${gradle.gradleUserHomeDir}" + } +} \ No newline at end of file diff --git a/Samples/SwiftJavaExtractJNISampleApp/ci-validate.sh b/Samples/SwiftJavaExtractJNISampleApp/ci-validate.sh new file mode 100755 index 000000000..d62a09102 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/ci-validate.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +set -x +set -e + +# WORKAROUND: prebuilts broken on Swift 6.2.1 and Linux and tests using macros https://github.com/swiftlang/swift-java/issues/418 +if [ "$(uname)" = "Darwin" ]; then + DISABLE_EXPERIMENTAL_PREBUILTS='' +else + DISABLE_EXPERIMENTAL_PREBUILTS='--disable-experimental-prebuilts' +fi + +if [[ "$(uname)" == "Darwin" && -n "$GITHUB_ACTION" ]]; then + # WORKAROUND: GitHub Actions on macOS issue with downloading gradle wrapper + # We seem to be hitting a problem when the swiftpm plugin, needs to execute gradle wrapper in a new gradle_user_home. + # Normally, this would just download gradle again and kick off a build, this seems to timeout *specifically* on + # github actions runners. + # + # It is not a sandbox problem, becuase the ./gradlew is run without sandboxing as we already execute + # the entire swift build with '--disable-sandbox' for other reasons. + # + # We cannot use the same gradle user home as the default one since we might make gradle think we're + # building the same project concurrently, which we kind of are, however only a limited subset in order + # to trigger wrap-java with those dependencies. + # + # TODO: this may use some further improvements so normal usage does not incur another wrapper download. + + ./gradlew -h # prime ~/.gradle/wrapper/dists/... + + # Worst part of workaround here; we make sure to pre-load the resolved gradle wrapper downloaded distribution + # to the "known" location the plugin will use for its local builds, which are done in order to compile SwiftKitCore. + # This build is only necessary in order to drive wrap-java on sources generated during the build itself + # which enables the "Implement Swift protocols in Java" feature of jextract/jni mode. + GRADLE_USER_HOME="$(pwd)/.build/plugins/outputs/swiftjavaextractjnisampleapp/MySwiftLibrary/destination/JExtractSwiftPlugin/gradle-user-home" + if [ -d "$HOME/.gradle" ] ; then + echo "COPY $HOME/.gradle to $GRADLE_USER_HOME" + mkdir -p "$GRADLE_USER_HOME" + cp -r "$HOME/.gradle/"* "$GRADLE_USER_HOME/" || true + fi +fi + +# FIXME: disable prebuilts until prebuilt swift-syntax isn't broken on 6.2 anymore: https://github.com/swiftlang/swift-java/issues/418 +swift build $DISABLE_EXPERIMENTAL_PREBUILTS --disable-sandbox + +./gradlew run +./gradlew test diff --git a/Samples/SwiftJavaExtractJNISampleApp/gradle.properties b/Samples/SwiftJavaExtractJNISampleApp/gradle.properties new file mode 120000 index 000000000..7677fb73b --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/gradle.properties @@ -0,0 +1 @@ +../gradle.properties \ No newline at end of file diff --git a/Samples/SwiftJavaExtractJNISampleApp/gradlew b/Samples/SwiftJavaExtractJNISampleApp/gradlew new file mode 120000 index 000000000..343e0d2ca --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/gradlew @@ -0,0 +1 @@ +../../gradlew \ No newline at end of file diff --git a/Samples/SwiftJavaExtractJNISampleApp/gradlew.bat b/Samples/SwiftJavaExtractJNISampleApp/gradlew.bat new file mode 120000 index 000000000..cb5a94645 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/gradlew.bat @@ -0,0 +1 @@ +../../gradlew.bat \ No newline at end of file diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/jmh/java/com/example/swift/AsyncBenchmark.java b/Samples/SwiftJavaExtractJNISampleApp/src/jmh/java/com/example/swift/AsyncBenchmark.java new file mode 100644 index 000000000..9cc74246f --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/src/jmh/java/com/example/swift/AsyncBenchmark.java @@ -0,0 +1,60 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package com.example.swift; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; +import org.swift.swiftkit.core.ClosableSwiftArena; +import org.swift.swiftkit.core.ConfinedSwiftMemorySession; +import org.swift.swiftkit.core.SwiftArena; + +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +@BenchmarkMode(Mode.AverageTime) +@Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 10, time = 500, timeUnit = TimeUnit.MILLISECONDS) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@State(Scope.Benchmark) +@Fork(value = 1, jvmArgsAppend = { "--enable-native-access=ALL-UNNAMED" }) +public class AsyncBenchmark { + /** + * Parameter for the number of parallel tasks to launch. + */ + @Param({"100", "500", "1000"}) + public int taskCount; + + @Setup(Level.Trial) + public void beforeAll() {} + + @TearDown(Level.Trial) + public void afterAll() {} + + @Benchmark + public void asyncSum(Blackhole bh) { + CompletableFuture[] futures = new CompletableFuture[taskCount]; + + // Launch all tasks in parallel using supplyAsync on our custom executor + for (int i = 0; i < taskCount; i++) { + futures[i] = MySwiftLibrary.asyncSum(10, 5).thenAccept(bh::consume); + } + + // Wait for all futures to complete. + CompletableFuture.allOf(futures).join(); + } +} \ No newline at end of file diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/jmh/java/com/example/swift/EnumBenchmark.java b/Samples/SwiftJavaExtractJNISampleApp/src/jmh/java/com/example/swift/EnumBenchmark.java new file mode 100644 index 000000000..d3de624be --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/src/jmh/java/com/example/swift/EnumBenchmark.java @@ -0,0 +1,60 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package com.example.swift; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; +import org.swift.swiftkit.core.ClosableSwiftArena; +import org.swift.swiftkit.core.ConfinedSwiftMemorySession; +import org.swift.swiftkit.core.SwiftArena; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.concurrent.TimeUnit; + +@BenchmarkMode(Mode.AverageTime) +@Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 10, time = 500, timeUnit = TimeUnit.MILLISECONDS) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@Fork(value = 3, jvmArgsAppend = { "--enable-native-access=ALL-UNNAMED" }) +public class EnumBenchmark { + + @State(Scope.Benchmark) + public static class BenchmarkState { + ClosableSwiftArena arena; + Vehicle vehicle; + + @Setup(Level.Trial) + public void beforeAll() { + arena = SwiftArena.ofConfined(); + vehicle = Vehicle.motorbike("Yamaha", 900, OptionalInt.empty(), arena); + } + + @TearDown(Level.Trial) + public void afterAll() { + arena.close(); + } + } + + @Benchmark + public Vehicle.Motorbike getAssociatedValues(BenchmarkState state, Blackhole bh) { + Vehicle.Motorbike motorbike = state.vehicle.getAsMotorbike().orElseThrow(); + bh.consume(motorbike.arg0()); + bh.consume(motorbike.horsePower()); + return motorbike; + } +} \ No newline at end of file diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/main/java/com/example/swift/HelloJava2SwiftJNI.java b/Samples/SwiftJavaExtractJNISampleApp/src/main/java/com/example/swift/HelloJava2SwiftJNI.java new file mode 100644 index 000000000..407ebe7ce --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/src/main/java/com/example/swift/HelloJava2SwiftJNI.java @@ -0,0 +1,63 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package com.example.swift; + +// Import swift-extract generated sources + +import org.swift.swiftkit.core.SwiftArena; +import org.swift.swiftkit.core.SwiftLibraries; + +public class HelloJava2SwiftJNI { + + public static void main(String[] args) { + System.out.print("Property: java.library.path = " + SwiftLibraries.getJavaLibraryPath()); + + examples(); + } + + static void examples() { + MySwiftLibrary.helloWorld(); + + MySwiftLibrary.globalTakeInt(1337); + MySwiftLibrary.globalTakeIntInt(1337, 42); + + long cnt = MySwiftLibrary.globalWriteString("String from Java"); + + long i = MySwiftLibrary.globalMakeInt(); + + MySwiftClass.method(); + + try (var arena = SwiftArena.ofConfined()) { + MySwiftClass myClass = MySwiftClass.init(10, 5, arena); + MySwiftClass myClass2 = MySwiftClass.init(arena); + + System.out.println("myClass.isWarm: " + myClass.isWarm()); + + try { + myClass.throwingFunction(); + } catch (Exception e) { + System.out.println("Caught exception: " + e.getMessage()); + } + + MySwiftStruct myStruct = MySwiftStruct.init(12, 34, arena); + System.out.println("myStruct.cap: " + myStruct.getCapacity()); + System.out.println("myStruct.len: " + myStruct.getLen()); + myStruct.increaseCap(10); + System.out.println("myStruct.cap after increase: " + myStruct.getCapacity()); + } + + System.out.println("DONE."); + } +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/AlignmentEnumTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/AlignmentEnumTest.java new file mode 100644 index 000000000..5bf059515 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/AlignmentEnumTest.java @@ -0,0 +1,42 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package com.example.swift; + +import org.junit.jupiter.api.Test; +import org.swift.swiftkit.core.ConfinedSwiftMemorySession; +import org.swift.swiftkit.core.JNISwiftInstance; +import org.swift.swiftkit.core.SwiftArena; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +public class AlignmentEnumTest { + @Test + void rawValue() { + try (var arena = SwiftArena.ofConfined()) { + Optional invalid = Alignment.init("invalid", arena); + assertFalse(invalid.isPresent()); + + Optional horizontal = Alignment.init("horizontal", arena); + assertTrue(horizontal.isPresent()); + assertEquals("horizontal", horizontal.get().getRawValue()); + + Optional vertical = Alignment.init("vertical", arena); + assertTrue(vertical.isPresent()); + assertEquals("vertical", vertical.get().getRawValue()); + } + } +} \ No newline at end of file diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ArraysTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ArraysTest.java new file mode 100644 index 000000000..bf19b370e --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ArraysTest.java @@ -0,0 +1,105 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package com.example.swift; + +import org.junit.jupiter.api.Test; +import org.swift.swiftkit.core.SwiftArena; + +import java.util.OptionalLong; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import static org.junit.jupiter.api.Assertions.*; + +public class ArraysTest { + @Test + void booleanArray() { + boolean[] input = new boolean[] { true, false, false, true }; + assertArrayEquals(input, MySwiftLibrary.booleanArray(input)); + } + + @Test + void byteArray() { + byte[] input = new byte[] { 10, 20, 30, 40 }; + assertArrayEquals(input, MySwiftLibrary.byteArray(input)); + } + + @Test + void byteArray_empty() { + byte[] input = new byte[] {}; + assertArrayEquals(input, MySwiftLibrary.byteArray(input)); + } + + @Test + void byteArray_null() { + assertThrows(NullPointerException.class, () -> MySwiftLibrary.byteArray(null)); + } + + @Test + void byteArrayExplicit() { + byte[] input = new byte[] { 10, 20, 30, 40 }; + assertArrayEquals(input, MySwiftLibrary.byteArrayExplicit(input)); + } + + @Test + void charArray() { + char[] input = new char[] { 10, 20, 30, 40 }; + assertArrayEquals(input, MySwiftLibrary.charArray(input)); + } + + @Test + void shortArray() { + short[] input = new short[] { 10, 20, 30, 40 }; + assertArrayEquals(input, MySwiftLibrary.shortArray(input)); + } + + @Test + void intArray() { + int[] input = new int[] { 10, 20, 30, 40 }; + assertArrayEquals(input, MySwiftLibrary.intArray(input)); + } + + @Test + void longArray() { + long[] input = new long[] { 10, 20, 30, 40 }; + assertArrayEquals(input, MySwiftLibrary.longArray(input)); + } + + @Test + void stringArray() { + String[] input = new String[] { "hey", "there", "my", "friend" }; + assertArrayEquals(input, MySwiftLibrary.stringArray(input)); + } + + @Test + void floatArray() { + float[] input = new float[] { 10, 20, 30, 40 }; + assertArrayEquals(input, MySwiftLibrary.floatArray(input)); + } + + @Test + void doubleArray() { + double[] input = new double[] { 10, 20, 30, 40 }; + assertArrayEquals(input, MySwiftLibrary.doubleArray(input)); + } + + @Test + void objectArray() { + try (var arena = SwiftArena.ofConfined()) { + MySwiftClass[] input = new MySwiftClass[]{MySwiftClass.init(arena), MySwiftClass.init(arena), MySwiftClass.init(arena) }; + assertEquals(3, MySwiftLibrary.objectArray(input, arena).length); + } + } +} \ No newline at end of file diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/AsyncTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/AsyncTest.java new file mode 100644 index 000000000..400844fdc --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/AsyncTest.java @@ -0,0 +1,84 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package com.example.swift; + +import com.example.swift.MySwiftClass; +import com.example.swift.MySwiftLibrary; +import org.junit.jupiter.api.Test; +import org.swift.swiftkit.core.SwiftArena; + +import java.time.Duration; +import java.util.Optional; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.OptionalLong; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +import static org.junit.jupiter.api.Assertions.*; + +public class AsyncTest { + @Test + void asyncSum() throws Exception { + Future future = MySwiftLibrary.asyncSum(10, 12); + + Long result = future.get(); + assertEquals(22, result); + } + + @Test + void asyncSleep() throws Exception { + Future future = MySwiftLibrary.asyncSleep(); + future.get(); + } + + @Test + void asyncCopy() throws Exception { + try (var arena = SwiftArena.ofConfined()) { + MySwiftClass obj = MySwiftClass.init(10, 5, arena); + Future future = MySwiftLibrary.asyncCopy(obj, arena); + + MySwiftClass result = future.get(); + + assertEquals(10, result.getX()); + assertEquals(5, result.getY()); + } + } + + @Test + void asyncThrows() { + Future future = MySwiftLibrary.asyncThrows(); + + ExecutionException ex = assertThrows(ExecutionException.class, future::get); + + Throwable cause = ex.getCause(); + assertNotNull(cause); + assertEquals(Exception.class, cause.getClass()); + assertEquals("swiftError", cause.getMessage()); + } + + @Test + void asyncOptional() throws Exception { + Future future = MySwiftLibrary.asyncOptional(42); + assertEquals(OptionalLong.of(42), future.get()); + } + + @Test + void asyncString() throws Exception { + Future future = MySwiftLibrary.asyncString("hey"); + assertEquals("hey", future.get()); + } +} \ No newline at end of file diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ClosuresTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ClosuresTest.java new file mode 100644 index 000000000..b8389d419 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ClosuresTest.java @@ -0,0 +1,44 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package com.example.swift; + +import org.junit.jupiter.api.Test; + +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.jupiter.api.Assertions.*; + +public class ClosuresTest { + @Test + void emptyClosure() { + AtomicBoolean closureCalled = new AtomicBoolean(false); + MySwiftLibrary.emptyClosure(() -> { + closureCalled.set(true); + }); + assertTrue(closureCalled.get()); + } + + @Test + void closureWithInt() { + long result = MySwiftLibrary.closureWithInt(10, (value) -> value * 2); + assertEquals(20, result); + } + + @Test + void closureMultipleArguments() { + long result = MySwiftLibrary.closureMultipleArguments(5, 10, (a, b) -> a + b); + assertEquals(15, result); + } +} \ No newline at end of file diff --git a/Samples/SwiftKitSampleApp/src/test/java/org/swift/swiftkit/MySwiftStructTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MultipleTypesFromSingleFileTest.java similarity index 51% rename from Samples/SwiftKitSampleApp/src/test/java/org/swift/swiftkit/MySwiftStructTest.java rename to Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MultipleTypesFromSingleFileTest.java index 843551a5f..ae02b8727 100644 --- a/Samples/SwiftKitSampleApp/src/test/java/org/swift/swiftkit/MySwiftStructTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MultipleTypesFromSingleFileTest.java @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -12,24 +12,25 @@ // //===----------------------------------------------------------------------===// -package org.swift.swiftkit; +package com.example.swift; -import com.example.swift.MySwiftStruct; import org.junit.jupiter.api.Test; +import org.swift.swiftkit.core.ConfinedSwiftMemorySession; +import org.swift.swiftkit.core.SwiftArena; -import static org.junit.jupiter.api.Assertions.assertEquals; +import java.lang.foreign.Arena; +import java.util.Optional; +import java.util.OptionalInt; -public class MySwiftStructTest { +import static org.junit.jupiter.api.Assertions.*; +public class MultipleTypesFromSingleFileTest { + @Test - void create_struct() { + void bothTypesMustHaveBeenGenerated() { try (var arena = SwiftArena.ofConfined()) { - long cap = 12; - long len = 34; - var struct = MySwiftStruct.init(cap, len, arena); - - assertEquals(cap, struct.getCapacity()); - assertEquals(len, struct.getLength()); + PublicTypeOne.init(arena); + PublicTypeTwo.init(arena); } } -} +} \ No newline at end of file diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java new file mode 100644 index 000000000..a1f8a4529 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java @@ -0,0 +1,190 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package com.example.swift; + +import org.junit.jupiter.api.Test; +import org.swift.swiftkit.core.SwiftArena; + +import java.util.Optional; +import java.util.OptionalInt; +import java.util.OptionalLong; + +import static org.junit.jupiter.api.Assertions.*; + +public class MySwiftClassTest { + @Test + void init_noParameters() { + try (var arena = SwiftArena.ofConfined()) { + MySwiftClass c = MySwiftClass.init(arena); + assertNotNull(c); + } + } + + @Test + void init_withParameters() { + try (var arena = SwiftArena.ofConfined()) { + MySwiftClass c = MySwiftClass.init(1337, 42, arena); + assertNotNull(c); + } + } + + @Test + void init_throwing() { + try (var arena = SwiftArena.ofConfined()) { + Exception exception = assertThrows(Exception.class, () -> MySwiftClass.init(true, arena)); + assertEquals("swiftError", exception.getMessage()); + + MySwiftClass c = assertDoesNotThrow(() -> MySwiftClass.init(false, arena)); + assertNotNull(c); + } + } + + @Test + void sum() { + try (var arena = SwiftArena.ofConfined()) { + MySwiftClass c = MySwiftClass.init(20, 10, arena); + assertEquals(30, c.sum()); + } + } + + @Test + void xMultiplied() { + try (var arena = SwiftArena.ofConfined()) { + MySwiftClass c = MySwiftClass.init(20, 10, arena); + assertEquals(200, c.xMultiplied(10)); + } + } + + @Test + void throwingFunction() { + try (var arena = SwiftArena.ofConfined()) { + MySwiftClass c = MySwiftClass.init(20, 10, arena); + Exception exception = assertThrows(Exception.class, () -> c.throwingFunction()); + + assertEquals("swiftError", exception.getMessage()); + } + } + + @Test + void constant() { + try (var arena = SwiftArena.ofConfined()) { + MySwiftClass c = MySwiftClass.init(20, 10, arena); + assertEquals(100, c.getConstant()); + } + } + + @Test + void mutable() { + try (var arena = SwiftArena.ofConfined()) { + MySwiftClass c = MySwiftClass.init(20, 10, arena); + assertEquals(0, c.getMutable()); + c.setMutable(42); + assertEquals(42, c.getMutable()); + } + } + + @Test + void product() { + try (var arena = SwiftArena.ofConfined()) { + MySwiftClass c = MySwiftClass.init(20, 10, arena); + assertEquals(200, c.getProduct()); + } + } + + @Test + void throwingVariable() { + try (var arena = SwiftArena.ofConfined()) { + MySwiftClass c = MySwiftClass.init(20, 10, arena); + + Exception exception = assertThrows(Exception.class, () -> c.getThrowingVariable()); + + assertEquals("swiftError", exception.getMessage()); + } + } + + @Test + void mutableDividedByTwo() { + try (var arena = SwiftArena.ofConfined()) { + MySwiftClass c = MySwiftClass.init(20, 10, arena); + assertEquals(0, c.getMutableDividedByTwo()); + c.setMutable(20); + assertEquals(10, c.getMutableDividedByTwo()); + c.setMutableDividedByTwo(5); + assertEquals(10, c.getMutable()); + } + } + + @Test + void isWarm() { + try (var arena = SwiftArena.ofConfined()) { + MySwiftClass c = MySwiftClass.init(20, 10, arena); + assertFalse(c.isWarm()); + } + } + + @Test + void sumWithX() { + try (var arena = SwiftArena.ofConfined()) { + MySwiftClass c1 = MySwiftClass.init(20, 10, arena); + MySwiftClass c2 = MySwiftClass.init(50, 10, arena); + assertEquals(70, c1.sumX(c2)); + } + } + + @Test + void copy() { + try (var arena = SwiftArena.ofConfined()) { + MySwiftClass c1 = MySwiftClass.init(20, 10, arena); + MySwiftClass c2 = c1.copy(arena); + + assertEquals(20, c2.getX()); + assertEquals(10, c2.getY()); + assertNotEquals(c1.$memoryAddress(), c2.$memoryAddress()); + } + } + + @Test + void addXWithJavaLong() { + try (var arena = SwiftArena.ofConfined()) { + MySwiftClass c1 = MySwiftClass.init(20, 10, arena); + Long javaLong = 50L; + assertEquals(70, c1.addXWithJavaLong(javaLong)); + } + } + + @Test + void getAsyncVariable() throws Exception { + try (var arena = SwiftArena.ofConfined()) { + MySwiftClass c1 = MySwiftClass.init(20, 10, arena); + assertEquals(42, c1.getGetAsync().get()); + } + } + + @Test + void toStringTest() { + try (var arena = SwiftArena.ofConfined()) { + MySwiftClass c1 = MySwiftClass.init(20, 10, arena); + assertEquals("MySwiftClass(x: 20, y: 10)", c1.toString()); + } + } + + @Test + void toDebugStringTest() { + try (var arena = SwiftArena.ofConfined()) { + MySwiftClass c1 = MySwiftClass.init(20, 10, arena); + assertEquals("debug: MySwiftClass(x: 20, y: 10)", c1.toDebugString()); + } + } +} \ No newline at end of file diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java new file mode 100644 index 000000000..6da2fd4b0 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java @@ -0,0 +1,71 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package com.example.swift; + +import com.example.swift.MySwiftLibrary; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.concurrent.CountDownLatch; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.*; + +public class MySwiftLibraryTest { + @Test + void call_helloWorld() { + MySwiftLibrary.helloWorld(); + } + + @Test + void call_globalTakeInt() { + MySwiftLibrary.globalTakeInt(12); + } + + @Test + void call_globalMakeInt() { + long i = MySwiftLibrary.globalMakeInt(); + assertEquals(42, i); + } + + @Test + void call_globalTakeIntInt() { + MySwiftLibrary.globalTakeIntInt(1337, 42); + } + + @Test + void call_writeString_jextract() { + var string = "Hello Swift!"; + long reply = MySwiftLibrary.globalWriteString(string); + + assertEquals(string.length(), reply); + } + + @Test + void globalVariable() { + assertEquals(0, MySwiftLibrary.getGlobalVariable()); + MySwiftLibrary.setGlobalVariable(100); + assertEquals(100, MySwiftLibrary.getGlobalVariable()); + } + + @Test + void globalUnsignedIntEcho() { + int i = 12; + long l = 1200; + assertEquals(1212, MySwiftLibrary.echoUnsignedInt(12, 1200)); + } +} \ No newline at end of file diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftStructTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftStructTest.java new file mode 100644 index 000000000..24b1fdbf9 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftStructTest.java @@ -0,0 +1,86 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package com.example.swift; + +import org.junit.jupiter.api.Test; +import org.swift.swiftkit.core.ConfinedSwiftMemorySession; +import org.swift.swiftkit.core.SwiftArena; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +public class MySwiftStructTest { + @Test + void init() { + try (var arena = SwiftArena.ofConfined()) { + MySwiftStruct s = MySwiftStruct.init(1337, 42, arena); + assertEquals(1337, s.getCapacity()); + assertEquals(42, s.getLen()); + } + } + + @Test + void init_optional() { + try (var arena = SwiftArena.ofConfined()) { + assertEquals(Optional.empty(), MySwiftStruct.init(false, arena)); + + Optional optionalStruct = MySwiftStruct.init(true, arena); + assertTrue(optionalStruct.isPresent()); + assertEquals(10, optionalStruct.get().getLen()); + } + } + + @Test + void getAndSetLen() { + try (var arena = SwiftArena.ofConfined()) { + MySwiftStruct s = MySwiftStruct.init(1337, 42, arena); + s.setLen(100); + assertEquals(100, s.getLen()); + } + } + + @Test + void increaseCap() { + try (var arena = SwiftArena.ofConfined()) { + MySwiftStruct s = MySwiftStruct.init(1337, 42, arena); + long newCap = s.increaseCap(10); + assertEquals(1347, newCap); + assertEquals(1347, s.getCapacity()); + } + } + + @Test + void testSubscript() { + try (var arena = SwiftArena.ofConfined()) { + MySwiftStruct s = MySwiftStruct.init(1337, 42, arena); + long currentValue = s.getSubscript(); + s.setSubscript(66); + assertEquals(0, currentValue); + assertEquals(66, s.getSubscriptValue()); + } + } + + @Test + void testSubscriptWithParams() { + try (var arena = SwiftArena.ofConfined()) { + MySwiftStruct s = MySwiftStruct.init(1337, 42, arena); + long currentValue = s.getSubscript(1); + s.setSubscript(1, 66); + assertEquals(20, currentValue); + assertEquals(66, s.getSubscriptArrayValue(1)); + } + } +} \ No newline at end of file diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/NestedTypesTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/NestedTypesTest.java new file mode 100644 index 000000000..b006ea914 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/NestedTypesTest.java @@ -0,0 +1,45 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package com.example.swift; + +import org.junit.jupiter.api.Test; +import org.swift.swiftkit.core.SwiftArena; + +import static org.junit.jupiter.api.Assertions.*; + +public class NestedTypesTest { + @Test + void testClassesAndStructs() { + try (var arena = SwiftArena.ofConfined()) { + var a = A.init(arena); + var b = A.B.init(arena); + var c = A.B.C.init(arena); + var bb = A.BB.init(arena); + var abbc = A.BB.C.init(arena); + + a.f(a, b, c, bb, abbc); + c.g(a, b, abbc); + } + } + + @Test + void testStructInEnum() { + try (var arena = SwiftArena.ofConfined()) { + var obj = NestedEnum.one(NestedEnum.OneStruct.init(arena), arena); + var one = obj.getAsOne(arena); + assertTrue(one.isPresent()); + } + } +} \ No newline at end of file diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/OptionalsTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/OptionalsTest.java new file mode 100644 index 000000000..d26de1d1f --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/OptionalsTest.java @@ -0,0 +1,125 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package com.example.swift; + +import com.example.swift.MySwiftLibrary; +import org.junit.jupiter.api.Test; +import org.swift.swiftkit.core.SwiftArena; + +import java.util.Optional; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.OptionalLong; + +import static org.junit.jupiter.api.Assertions.*; + +public class OptionalsTest { + @Test + void optionalBool() { + assertEquals(Optional.empty(), MySwiftLibrary.optionalBool(Optional.empty())); + assertEquals(Optional.of(true), MySwiftLibrary.optionalBool(Optional.of(true))); + } + + @Test + void optionalByte() { + assertEquals(Optional.empty(), MySwiftLibrary.optionalByte(Optional.empty())); + assertEquals(Optional.of((byte) 1) , MySwiftLibrary.optionalByte(Optional.of((byte) 1))); + } + + @Test + void optionalChar() { + assertEquals(Optional.empty(), MySwiftLibrary.optionalChar(Optional.empty())); + assertEquals(Optional.of((char) 42), MySwiftLibrary.optionalChar(Optional.of((char) 42))); + } + + @Test + void optionalShort() { + assertEquals(Optional.empty(), MySwiftLibrary.optionalShort(Optional.empty())); + assertEquals(Optional.of((short) -250), MySwiftLibrary.optionalShort(Optional.of((short) -250))); + } + + @Test + void optionalInt() { + assertEquals(OptionalInt.empty(), MySwiftLibrary.optionalInt(OptionalInt.empty())); + assertEquals(OptionalInt.of(999), MySwiftLibrary.optionalInt(OptionalInt.of(999))); + } + + @Test + void optionalLong() { + assertEquals(OptionalLong.empty(), MySwiftLibrary.optionalLong(OptionalLong.empty())); + assertEquals(OptionalLong.of(999), MySwiftLibrary.optionalLong(OptionalLong.of(999))); + } + + @Test + void optionalFloat() { + assertEquals(Optional.empty(), MySwiftLibrary.optionalFloat(Optional.empty())); + assertEquals(Optional.of(3.14f), MySwiftLibrary.optionalFloat(Optional.of(3.14f))); + } + + @Test + void optionalDouble() { + assertEquals(OptionalDouble.empty(), MySwiftLibrary.optionalDouble(OptionalDouble.empty())); + assertEquals(OptionalDouble.of(2.718), MySwiftLibrary.optionalDouble(OptionalDouble.of(2.718))); + } + + @Test + void optionalString() { + assertEquals(Optional.empty(), MySwiftLibrary.optionalString(Optional.empty())); + assertEquals(Optional.of("Hello Swift!"), MySwiftLibrary.optionalString(Optional.of("Hello Swift!"))); + } + + @Test + void optionalClass() { + try (var arena = SwiftArena.ofConfined()) { + MySwiftClass c = MySwiftClass.init(arena); + assertEquals(Optional.empty(), MySwiftLibrary.optionalClass(Optional.empty(), arena)); + Optional optionalClass = MySwiftLibrary.optionalClass(Optional.of(c), arena); + assertTrue(optionalClass.isPresent()); + assertEquals(c.getX(), optionalClass.get().getX()); + } + } + + @Test + void optionalJavaKitLong() { + assertEquals(OptionalLong.empty(), MySwiftLibrary.optionalJavaKitLong(Optional.empty())); + assertEquals(OptionalLong.of(99L), MySwiftLibrary.optionalJavaKitLong(Optional.of(99L))); + } + + @Test + void multipleOptionals() { + try (var arena = SwiftArena.ofConfined()) { + MySwiftClass c = MySwiftClass.init(arena); + OptionalLong result = MySwiftLibrary.multipleOptionals( + Optional.of((byte) 1), + Optional.of((short) 42), + OptionalInt.of(50), + OptionalLong.of(1000L), + Optional.of("42"), + Optional.of(c), + Optional.of(true) + ); + assertEquals(result, OptionalLong.of(1L)); + } + } + + @Test + void optionalThrows() { + try (var arena = SwiftArena.ofConfined()) { + Exception exception = assertThrows(Exception.class, () -> MySwiftLibrary.optionalThrowing()); + + assertEquals("swiftError", exception.getMessage()); + } + } +} \ No newline at end of file diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolCallbacksTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolCallbacksTest.java new file mode 100644 index 000000000..b4ebe8532 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolCallbacksTest.java @@ -0,0 +1,117 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package com.example.swift; + +import org.junit.jupiter.api.Test; +import org.swift.swiftkit.core.SwiftArena; +import org.swift.swiftkit.core.annotations.Unsigned; + +import java.util.Optional; +import java.util.OptionalLong; + +import static org.junit.jupiter.api.Assertions.*; + +public class ProtocolCallbacksTest { + static class JavaCallbacks implements CallbackProtocol { + @Override + public boolean withBool(boolean input) { + return input; + } + + @Override + public byte withInt8(byte input) { + return input; + } + + @Override + public @Unsigned char withUInt16(char input) { + return input; + } + + @Override + public short withInt16(short input) { + return input; + } + + @Override + public int withInt32(int input) { + return input; + } + + @Override + public long withInt64(long input) { + return input; + } + + @Override + public float withFloat(float input) { + return input; + } + + @Override + public double withDouble(double input) { + return input; + } + + @Override + public String withString(String input) { + return input; + } + + @Override + public void withVoid() {} + + @Override + public MySwiftClass withObject(MySwiftClass input, SwiftArena swiftArena$) { + return input; + } + + @Override + public OptionalLong withOptionalInt64(OptionalLong input) { + return input; + } + + @Override + public Optional withOptionalObject(Optional input, SwiftArena swiftArena$) { + return input; + } + } + + @Test + void primitiveCallbacks() { + try (var arena = SwiftArena.ofConfined()) { + JavaCallbacks callbacks = new JavaCallbacks(); + var object = MySwiftClass.init(5, 3, arena); + var optionalObject = Optional.of(MySwiftClass.init(10, 10, arena)); + var output = MySwiftLibrary.outputCallbacks(callbacks, true, (byte) 1, (char) 16, (short) 16, (int) 32, 64L, 1.34f, 1.34, "Hello from Java!", object, OptionalLong.empty(), optionalObject, arena); + + assertEquals(1, output.getInt8()); + assertEquals(16, output.getUint16()); + assertEquals(16, output.getInt16()); + assertEquals(32, output.getInt32()); + assertEquals(64, output.getInt64()); + assertEquals(1.34f, output.get_float()); + assertEquals(1.34, output.get_double()); + assertEquals("Hello from Java!", output.getString()); + assertFalse(output.getOptionalInt64().isPresent()); + assertEquals(5, output.getObject(arena).getX()); + assertEquals(3, output.getObject(arena).getY()); + + var optionalObjectOutput = output.getOptionalObject(arena); + assertTrue(optionalObjectOutput.isPresent()); + assertEquals(10, optionalObjectOutput.get().getX()); + } + } +} \ No newline at end of file diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolTest.java new file mode 100644 index 000000000..b8159b8ad --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolTest.java @@ -0,0 +1,113 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package com.example.swift; + +import org.junit.jupiter.api.Test; +import org.swift.swiftkit.core.SwiftArena; + +import static org.junit.jupiter.api.Assertions.*; + +public class ProtocolTest { + @Test + void takeProtocol() { + try (var arena = SwiftArena.ofConfined()) { + ConcreteProtocolAB proto1 = ConcreteProtocolAB.init(10, 5, arena); + ConcreteProtocolAB proto2 = ConcreteProtocolAB.init(20, 1, arena); + assertEquals(30, MySwiftLibrary.takeProtocol(proto1, proto2)); + } + } + + @Test + void takeCombinedProtocol() { + try (var arena = SwiftArena.ofConfined()) { + ConcreteProtocolAB proto1 = ConcreteProtocolAB.init(10, 5, arena); + assertEquals(15, MySwiftLibrary.takeCombinedProtocol(proto1)); + } + } + + @Test + void takeGenericProtocol() { + try (var arena = SwiftArena.ofConfined()) { + ConcreteProtocolAB proto1 = ConcreteProtocolAB.init(10, 5, arena); + ConcreteProtocolAB proto2 = ConcreteProtocolAB.init(20, 1, arena); + assertEquals(11, MySwiftLibrary.takeGenericProtocol(proto1, proto2)); + } + } + + @Test + void takeCombinedGenericProtocol() { + try (var arena = SwiftArena.ofConfined()) { + ConcreteProtocolAB proto1 = ConcreteProtocolAB.init(10, 5, arena); + assertEquals(15, MySwiftLibrary.takeCombinedGenericProtocol(proto1)); + } + } + + @Test + void protocolVariables() { + try (var arena = SwiftArena.ofConfined()) { + ProtocolA proto1 = ConcreteProtocolAB.init(10, 5, arena); + assertEquals(10, proto1.getConstantA()); + assertEquals(0, proto1.getMutable()); + proto1.setMutable(3); + assertEquals(3, proto1.getMutable()); + } + } + + @Test + void protocolMethod() { + try (var arena = SwiftArena.ofConfined()) { + ProtocolA proto1 = ConcreteProtocolAB.init(10, 5, arena); + assertEquals("ConcreteProtocolAB", proto1.name()); + } + } + + @Test + void protocolClassMethod() { + try (var arena = SwiftArena.ofConfined()) { + ProtocolA proto1 = ConcreteProtocolAB.init(10, 5, arena); + assertEquals(10, proto1.makeClass().getX()); + } + } + + static class JavaStorage implements Storage { + StorageItem item; + + JavaStorage(StorageItem item) { + this.item = item; + } + + @Override + public StorageItem load(SwiftArena swiftArena$) { + return item; + } + + @Override + public void save(StorageItem item) { + this.item = item; + } + } + + @Test + void useStorage() { + try (var arena = SwiftArena.ofConfined()) { + JavaStorage storage = new JavaStorage(null); + MySwiftLibrary.saveWithStorage(StorageItem.init(10, arena), storage); + assertEquals(10, MySwiftLibrary.loadWithStorage(storage, arena).getValue()); + MySwiftLibrary.saveWithStorage(StorageItem.init(7, arena), storage); + MySwiftLibrary.saveWithStorage(StorageItem.init(5, arena), storage); + assertEquals(5, MySwiftLibrary.loadWithStorage(storage, arena).getValue()); + } + } +} \ No newline at end of file diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/VehicleEnumTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/VehicleEnumTest.java new file mode 100644 index 000000000..c533603a2 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/VehicleEnumTest.java @@ -0,0 +1,206 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package com.example.swift; + +import org.junit.jupiter.api.Test; +import org.swift.swiftkit.core.ConfinedSwiftMemorySession; +import org.swift.swiftkit.core.SwiftArena; + +import java.lang.foreign.Arena; +import java.util.Optional; +import java.util.OptionalInt; + +import static org.junit.jupiter.api.Assertions.*; + +public class VehicleEnumTest { + @Test + void bicycle() { + try (var arena = SwiftArena.ofConfined()) { + Vehicle vehicle = Vehicle.bicycle(arena); + assertNotNull(vehicle); + } + } + + @Test + void car() { + try (var arena = SwiftArena.ofConfined()) { + Vehicle vehicle = Vehicle.car("Porsche 911", Optional.empty(), arena); + assertNotNull(vehicle); + } + } + + @Test + void motorbike() { + try (var arena = SwiftArena.ofConfined()) { + Vehicle vehicle = Vehicle.motorbike("Yamaha", 750, OptionalInt.empty(), arena); + assertNotNull(vehicle); + } + } + + @Test + void initName() { + try (var arena = SwiftArena.ofConfined()) { + assertFalse(Vehicle.init("bus", arena).isPresent()); + Optional vehicle = Vehicle.init("car", arena); + assertTrue(vehicle.isPresent()); + assertNotNull(vehicle.get()); + } + } + + @Test + void nameProperty() { + try (var arena = SwiftArena.ofConfined()) { + Vehicle vehicle = Vehicle.bicycle(arena); + assertEquals("bicycle", vehicle.getName()); + } + } + + @Test + void isFasterThan() { + try (var arena = SwiftArena.ofConfined()) { + Vehicle bicycle = Vehicle.bicycle(arena); + Vehicle car = Vehicle.car("Porsche 911", Optional.empty(), arena); + assertFalse(bicycle.isFasterThan(car)); + assertTrue(car.isFasterThan(bicycle)); + } + } + + @Test + void upgrade() { + try (var arena = SwiftArena.ofConfined()) { + Vehicle vehicle = Vehicle.bicycle(arena); + assertEquals("bicycle", vehicle.getName()); + vehicle.upgrade(); + assertEquals("car", vehicle.getName()); + vehicle.upgrade(); + assertEquals("motorbike", vehicle.getName()); + } + } + + @Test + void getAsBicycle() { + try (var arena = SwiftArena.ofConfined()) { + Vehicle vehicle = Vehicle.bicycle(arena); + Vehicle.Bicycle bicycle = vehicle.getAsBicycle().orElseThrow(); + assertNotNull(bicycle); + } + } + + @Test + void getAsCar() { + try (var arena = SwiftArena.ofConfined()) { + Vehicle vehicle = Vehicle.car("BMW", Optional.empty(), arena); + Vehicle.Car car = vehicle.getAsCar().orElseThrow(); + assertEquals("BMW", car.arg0()); + + vehicle = Vehicle.car("BMW", Optional.of("Long trailer"), arena); + car = vehicle.getAsCar().orElseThrow(); + assertEquals("Long trailer", car.trailer().orElseThrow()); + } + } + + @Test + void getAsMotorbike() { + try (var arena = SwiftArena.ofConfined()) { + Vehicle vehicle = Vehicle.motorbike("Yamaha", 750, OptionalInt.empty(), arena); + Vehicle.Motorbike motorbike = vehicle.getAsMotorbike().orElseThrow(); + assertEquals("Yamaha", motorbike.arg0()); + assertEquals(750, motorbike.horsePower()); + assertEquals(OptionalInt.empty(), motorbike.helmets()); + + vehicle = Vehicle.motorbike("Yamaha", 750, OptionalInt.of(2), arena); + motorbike = vehicle.getAsMotorbike().orElseThrow(); + assertEquals(OptionalInt.of(2), motorbike.helmets()); + } + } + + @Test + void getAsTransformer() { + try (var arena = SwiftArena.ofConfined()) { + Vehicle vehicle = Vehicle.transformer(Vehicle.bicycle(arena), Vehicle.car("BMW", Optional.empty(), arena), arena); + Vehicle.Transformer transformer = vehicle.getAsTransformer(arena).orElseThrow(); + assertTrue(transformer.front().getAsBicycle().isPresent()); + assertEquals("BMW", transformer.back().getAsCar().orElseThrow().arg0()); + } + } + + @Test + void getAsBoat() { + try (var arena = SwiftArena.ofConfined()) { + Vehicle vehicle = Vehicle.boat(OptionalInt.of(10), Optional.of((short) 1), arena); + Vehicle.Boat boat = vehicle.getAsBoat().orElseThrow(); + assertEquals(OptionalInt.of(10), boat.passengers()); + assertEquals(Optional.of((short) 1), boat.length()); + } + } + + @Test + void associatedValuesAreCopied() { + try (var arena = SwiftArena.ofConfined()) { + Vehicle vehicle = Vehicle.car("BMW", Optional.empty(), arena); + Vehicle.Car car = vehicle.getAsCar().orElseThrow(); + assertEquals("BMW", car.arg0()); + vehicle.upgrade(); + Vehicle.Motorbike motorbike = vehicle.getAsMotorbike().orElseThrow(); + assertNotNull(motorbike); + // Motorbike should still remain + assertEquals("BMW", car.arg0()); + } + } + + @Test + void getDiscriminator() { + try (var arena = SwiftArena.ofConfined()) { + assertEquals(Vehicle.Discriminator.BICYCLE, Vehicle.bicycle(arena).getDiscriminator()); + assertEquals(Vehicle.Discriminator.CAR, Vehicle.car("BMW", Optional.empty(), arena).getDiscriminator()); + assertEquals(Vehicle.Discriminator.MOTORBIKE, Vehicle.motorbike("Yamaha", 750, OptionalInt.empty(), arena).getDiscriminator()); + assertEquals(Vehicle.Discriminator.TRANSFORMER, Vehicle.transformer(Vehicle.bicycle(arena), Vehicle.bicycle(arena), arena).getDiscriminator()); + } + } + + @Test + void getCase() { + try (var arena = SwiftArena.ofConfined()) { + Vehicle vehicle = Vehicle.bicycle(arena); + Vehicle.Case caseElement = vehicle.getCase(arena); + assertInstanceOf(Vehicle.Bicycle.class, caseElement); + } + } + + @Test + void switchGetCase() { + try (var arena = SwiftArena.ofConfined()) { + Vehicle vehicle = Vehicle.car("BMW", Optional.empty(), arena); + switch (vehicle.getCase(arena)) { + case Vehicle.Bicycle b: + fail("Was bicycle"); + break; + case Vehicle.Car car: + assertEquals("BMW", car.arg0()); + break; + case Vehicle.Motorbike motorbike: + fail("Was motorbike"); + break; + case Vehicle.Transformer transformer: + fail("Was transformer"); + break; + case Vehicle.Boat b: + fail("Was boat"); + break; + } + } + } + +} \ No newline at end of file diff --git a/Samples/SwiftKitSampleApp/Sources/MySwiftLibrary/swift-java.config b/Samples/SwiftKitSampleApp/Sources/MySwiftLibrary/swift-java.config deleted file mode 100644 index 6e5bc2af1..000000000 --- a/Samples/SwiftKitSampleApp/Sources/MySwiftLibrary/swift-java.config +++ /dev/null @@ -1,3 +0,0 @@ -{ - "javaPackage": "com.example.swift" -} diff --git a/Samples/SwiftKitSampleApp/src/main/java/com/example/swift/HelloJava2Swift.java b/Samples/SwiftKitSampleApp/src/main/java/com/example/swift/HelloJava2Swift.java deleted file mode 100644 index 69dfbdb34..000000000 --- a/Samples/SwiftKitSampleApp/src/main/java/com/example/swift/HelloJava2Swift.java +++ /dev/null @@ -1,82 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift.org project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift.org project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -package com.example.swift; - -// Import swift-extract generated sources - -// Import javakit/swiftkit support libraries - -import org.swift.swiftkit.SwiftArena; -import org.swift.swiftkit.SwiftKit; - -public class HelloJava2Swift { - - public static void main(String[] args) { - boolean traceDowncalls = Boolean.getBoolean("jextract.trace.downcalls"); - System.out.println("Property: jextract.trace.downcalls = " + traceDowncalls); - - System.out.print("Property: java.library.path = " + SwiftKit.getJavaLibraryPath()); - - examples(); - } - - static void examples() { - MySwiftLibrary.helloWorld(); - - MySwiftLibrary.globalTakeInt(1337); - - long cnt = MySwiftLibrary.globalWriteString("String from Java"); - - SwiftKit.trace("count = " + cnt); - - MySwiftLibrary.globalCallMeRunnable(() -> { - SwiftKit.trace("running runnable"); - }); - - // Example of using an arena; MyClass.deinit is run at end of scope - try (var arena = SwiftArena.ofConfined()) { - MySwiftClass obj = MySwiftClass.init(2222, 7777, arena); - - // just checking retains/releases work - SwiftKit.trace("retainCount = " + SwiftKit.retainCount(obj)); - SwiftKit.retain(obj); - SwiftKit.trace("retainCount = " + SwiftKit.retainCount(obj)); - SwiftKit.release(obj); - SwiftKit.trace("retainCount = " + SwiftKit.retainCount(obj)); - - obj.setCounter(12); - SwiftKit.trace("obj.counter = " + obj.getCounter()); - - obj.voidMethod(); - obj.takeIntMethod(42); - - MySwiftClass otherObj = MySwiftClass.factory(12, 42, arena); - otherObj.voidMethod(); - - MySwiftStruct swiftValue = MySwiftStruct.init(2222, 1111, arena); - SwiftKit.trace("swiftValue.capacity = " + swiftValue.getCapacity()); - swiftValue.withCapLen((cap, len) -> { - SwiftKit.trace("withCapLenCallback: cap=" + cap + ", len=" + len); - }); - } - - System.out.println("DONE."); - } - - public static native long jniWriteString(String str); - - public static native long jniGetInt(); - -} diff --git a/Samples/gradle.properties b/Samples/gradle.properties new file mode 120000 index 000000000..7677fb73b --- /dev/null +++ b/Samples/gradle.properties @@ -0,0 +1 @@ +../gradle.properties \ No newline at end of file diff --git a/Sources/CSwiftJavaJNI/AndroidSupport.cpp b/Sources/CSwiftJavaJNI/AndroidSupport.cpp new file mode 100644 index 000000000..a223530a1 --- /dev/null +++ b/Sources/CSwiftJavaJNI/AndroidSupport.cpp @@ -0,0 +1,98 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +#ifdef __ANDROID__ + +#include +#include +#include + +// these are not exported by the Android SDK + +extern "C" { + using JavaRuntime_GetDefaultJavaVMInitArgs_fn = jint (*)(void *vm_args); + using JavaRuntime_CreateJavaVM_fn = jint (*)(JavaVM **, JNIEnv **, void *); + using JavaRuntime_GetCreatedJavaVMs_fn = jint (*)(JavaVM **, jsize, jsize *); +} + +static JavaRuntime_GetDefaultJavaVMInitArgs_fn + JavaRuntime_GetDefaultJavaVMInitArgs; +static JavaRuntime_CreateJavaVM_fn JavaRuntime_CreateJavaVM; +static JavaRuntime_GetCreatedJavaVMs_fn JavaRuntime_GetCreatedJavaVMs; + +static void *JavaRuntime_dlhandle; + +__attribute__((constructor)) static void JavaRuntime_init(void) { + JavaRuntime_dlhandle = dlopen("libnativehelper.so", RTLD_NOW | RTLD_LOCAL); + if (JavaRuntime_dlhandle == nullptr) { + __android_log_print(ANDROID_LOG_FATAL, "JavaRuntime", + "failed to open libnativehelper.so: %s", dlerror()); + return; + } + + JavaRuntime_GetDefaultJavaVMInitArgs = + reinterpret_cast( + dlsym(JavaRuntime_dlhandle, "JNI_GetDefaultJavaVMInitArgs")); + if (JavaRuntime_GetDefaultJavaVMInitArgs == nullptr) + __android_log_print(ANDROID_LOG_FATAL, "JavaRuntime", + "JNI_GetDefaultJavaVMInitArgs not found: %s", + dlerror()); + + JavaRuntime_CreateJavaVM = reinterpret_cast( + dlsym(JavaRuntime_dlhandle, "JNI_CreateJavaVM")); + if (JavaRuntime_CreateJavaVM == nullptr) + __android_log_print(ANDROID_LOG_FATAL, "JavaRuntime", + "JNI_CreateJavaVM not found: %s", dlerror()); + + JavaRuntime_GetCreatedJavaVMs = + reinterpret_cast( + dlsym(JavaRuntime_dlhandle, "JNI_GetCreatedJavaVMs")); + if (JavaRuntime_GetCreatedJavaVMs == nullptr) + __android_log_print(ANDROID_LOG_FATAL, "JavaRuntime", + "JNI_GetCreatedJavaVMs not found: %s", dlerror()); +} + +__attribute__((destructor)) static void JavaRuntime_deinit(void) { + if (JavaRuntime_dlhandle) { + dlclose(JavaRuntime_dlhandle); + JavaRuntime_dlhandle = nullptr; + } + + JavaRuntime_GetDefaultJavaVMInitArgs = nullptr; + JavaRuntime_CreateJavaVM = nullptr; + JavaRuntime_GetCreatedJavaVMs = nullptr; +} + +jint JNI_GetDefaultJavaVMInitArgs(void *vm_args) { + if (JavaRuntime_GetDefaultJavaVMInitArgs == nullptr) + return JNI_ERR; + + return (*JavaRuntime_GetDefaultJavaVMInitArgs)(vm_args); +} + +jint JNI_CreateJavaVM(JavaVM **vm, JNIEnv **env, void *vm_args) { + if (JavaRuntime_CreateJavaVM == nullptr) + return JNI_ERR; + + return (*JavaRuntime_CreateJavaVM)(vm, env, vm_args); +} + +jint JNI_GetCreatedJavaVMs(JavaVM **vmBuf, jsize bufLen, jsize *nVMs) { + if (JavaRuntime_GetCreatedJavaVMs == nullptr) + return JNI_ERR; + + return (*JavaRuntime_GetCreatedJavaVMs)(vmBuf, bufLen, nVMs); +} + +#endif diff --git a/Sources/JavaRuntime/dummy.c b/Sources/CSwiftJavaJNI/dummy.c similarity index 100% rename from Sources/JavaRuntime/dummy.c rename to Sources/CSwiftJavaJNI/dummy.c diff --git a/Sources/JavaRuntime/include/JavaRuntime.h b/Sources/CSwiftJavaJNI/include/CSwiftJavaJNI.h similarity index 85% rename from Sources/JavaRuntime/include/JavaRuntime.h rename to Sources/CSwiftJavaJNI/include/CSwiftJavaJNI.h index 02bf548e4..fb5f71afd 100644 --- a/Sources/JavaRuntime/include/JavaRuntime.h +++ b/Sources/CSwiftJavaJNI/include/CSwiftJavaJNI.h @@ -12,9 +12,9 @@ // //===----------------------------------------------------------------------===// -#ifndef Swift_JavaRuntime_h -#define Swift_JavaRuntime_h +#ifndef CSwiftJavaJNI_h +#define CSwiftJavaJNI_h #include -#endif /* Swift_JavaRuntime_h */ +#endif /* CSwiftJavaJNI_h */ diff --git a/Sources/CSwiftJavaJNI/include/module.modulemap b/Sources/CSwiftJavaJNI/include/module.modulemap new file mode 100644 index 000000000..2be3eda91 --- /dev/null +++ b/Sources/CSwiftJavaJNI/include/module.modulemap @@ -0,0 +1,4 @@ +module CSwiftJavaJNI { + umbrella header "CSwiftJavaJNI.h" + export * +} diff --git a/Sources/ExampleSwiftLibrary/MySwiftLibrary.swift b/Sources/ExampleSwiftLibrary/MySwiftLibrary.swift index 96afd331d..1c9477f11 100644 --- a/Sources/ExampleSwiftLibrary/MySwiftLibrary.swift +++ b/Sources/ExampleSwiftLibrary/MySwiftLibrary.swift @@ -41,6 +41,10 @@ public func globalCallMeRunnable(run: () -> ()) { run() } +public func globalReceiveRawBuffer(buf: UnsafeRawBufferPointer) -> Int { + return buf.count +} + public class MySwiftClass { public var len: Int diff --git a/Sources/JExtractSwiftLib/CodePrinter.swift b/Sources/JExtractSwiftLib/CodePrinter.swift index 449cab3da..1497a61ab 100644 --- a/Sources/JExtractSwiftLib/CodePrinter.swift +++ b/Sources/JExtractSwiftLib/CodePrinter.swift @@ -53,14 +53,14 @@ public struct CodePrinter { self.mode = mode } - internal mutating func append(_ text: String) { + mutating func append(_ text: String) { contents.append(text) if self.verbose { Swift.print(text, terminator: "") } } - internal mutating func append(contentsOf text: S) + mutating func append(contentsOf text: S) where S: Sequence, S.Element == Character { contents.append(contentsOf: text) if self.verbose { @@ -70,14 +70,33 @@ public struct CodePrinter { } } + public mutating func printHashIfBlock( + _ header: Any, + function: String = #function, + file: String = #fileID, + line: UInt = #line, + body: (inout CodePrinter) throws -> () + ) rethrows { + print("#if \(header)") + indent() + try body(&self) + outdent() + print("#endif // end of \(header)", .sloc, function: function, file: file, line: line) + } + public mutating func printBraceBlock( _ header: Any, + parameters: [String]? = nil, function: String = #function, file: String = #fileID, line: UInt = #line, body: (inout CodePrinter) throws -> () ) rethrows { - print("\(header) {") + print("\(header) {", .continue) + if let parameters { + print(" (\(parameters.joined(separator: ", "))) in", .continue) + } + println() indent() try body(&self) outdent() @@ -216,14 +235,28 @@ extension CodePrinter { /// - Returns: the output path of the generated file, if any (i.e. not in accumulate in memory mode) package mutating func writeContents( - outputDirectory: String, + outputDirectory _outputDirectory: String, javaPackagePath: String?, - filename: String + filename _filename: String ) throws -> URL? { + + // We handle 'filename' that has a path, since that simplifies passing paths from root output directory enourmously. + // This just moves the directory parts into the output directory part in order for us to create the sub-directories. + let outputDirectory: String + let filename: String + if _filename.contains(PATH_SEPARATOR) { + let parts = _filename.split(separator: PATH_SEPARATOR) + outputDirectory = _outputDirectory.appending(PATH_SEPARATOR).appending(parts.dropLast().joined(separator: PATH_SEPARATOR)) + filename = "\(parts.last!)" + } else { + outputDirectory = _outputDirectory + filename = _filename + } + guard self.mode != .accumulateAll else { // if we're accumulating everything, we don't want to finalize/flush any contents // let's mark that this is where a write would have happened though: - print("// ^^^^ Contents of: \(outputDirectory)/\(filename)") + print("// ^^^^ Contents of: \(outputDirectory)\(PATH_SEPARATOR)\(filename)") return nil } @@ -233,7 +266,7 @@ extension CodePrinter { "// ==== ---------------------------------------------------------------------------------------------------" ) if let javaPackagePath { - print("// \(javaPackagePath)/\(filename)") + print("// \(javaPackagePath)\(PATH_SEPARATOR)\(filename)") } else { print("// \(filename)") } @@ -242,9 +275,15 @@ extension CodePrinter { } let targetDirectory = [outputDirectory, javaPackagePath].compactMap { $0 }.joined(separator: PATH_SEPARATOR) - log.trace("Prepare target directory: \(targetDirectory)") - try FileManager.default.createDirectory( - atPath: targetDirectory, withIntermediateDirectories: true) + log.debug("Prepare target directory: '\(targetDirectory)' for file \(filename.bold)") + do { + try FileManager.default.createDirectory( + atPath: targetDirectory, withIntermediateDirectories: true) + } catch { + // log and throw since it can be confusing what the reason for failing the write was otherwise + log.warning("Failed to create directory: \(targetDirectory)") + throw error + } let outputPath = Foundation.URL(fileURLWithPath: targetDirectory).appendingPathComponent(filename) try contents.write( diff --git a/Sources/JExtractSwiftLib/Common/TypeAnnotations.swift b/Sources/JExtractSwiftLib/Common/TypeAnnotations.swift new file mode 100644 index 000000000..0896e4be6 --- /dev/null +++ b/Sources/JExtractSwiftLib/Common/TypeAnnotations.swift @@ -0,0 +1,26 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JavaTypes +import SwiftJavaConfigurationShared + +/// Determine if the given type needs any extra annotations that should be included +/// in Java sources when the corresponding Java type is rendered. +func getTypeAnnotations(swiftType: SwiftType, config: Configuration) -> [JavaAnnotation] { + if swiftType.isUnsignedInteger, config.effectiveUnsignedNumbersMode == .annotate { + return [JavaAnnotation.unsigned] + } + + return [] +} diff --git a/Sources/JExtractSwiftLib/Configuration+Extensions.swift b/Sources/JExtractSwiftLib/Configuration+Extensions.swift new file mode 100644 index 000000000..042d86100 --- /dev/null +++ b/Sources/JExtractSwiftLib/Configuration+Extensions.swift @@ -0,0 +1,25 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftJavaConfigurationShared +import JavaTypes + +extension Configuration { + public var effectiveUnsignedNumericsMode: UnsignedNumericsMode { + switch effectiveUnsignedNumbersMode { + case .annotate: .ignoreSign + case .wrapGuava: .wrapUnsignedGuava + } + } +} \ No newline at end of file diff --git a/Sources/JExtractSwiftLib/Convenience/JavaType+Extensions.swift b/Sources/JExtractSwiftLib/Convenience/JavaType+Extensions.swift new file mode 100644 index 000000000..9f7a19cce --- /dev/null +++ b/Sources/JExtractSwiftLib/Convenience/JavaType+Extensions.swift @@ -0,0 +1,149 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JavaTypes + +extension JavaType { + var jniTypeSignature: String { + switch self { + case .boolean: return "Z" + case .byte: return "B" + case .char: return "C" + case .short: return "S" + case .int: return "I" + case .long: return "J" + case .float: return "F" + case .double: return "D" + case .class(let package, let name, _): + let nameWithInnerClasses = name.replacingOccurrences(of: ".", with: "$") + if let package { + return "L\(package.replacingOccurrences(of: ".", with: "/"))/\(nameWithInnerClasses);" + } else { + return "L\(nameWithInnerClasses);" + } + case .array(let javaType): return "[\(javaType.jniTypeSignature)" + case .void: fatalError("There is no type signature for 'void'") + } + } + + /// Returns the next integral type with space for self and an additional byte. + var nextIntergralTypeWithSpaceForByte: (javaType: JavaType, swiftType: SwiftKnownTypeDeclKind, valueBytes: Int)? { + switch self { + case .boolean, .byte: (.short, .int16, 1) + case .char, .short: (.int, .int32, 2) + case .int: (.long, .int64, 4) + default: nil + } + } + + var optionalType: String? { + switch self { + case .boolean: "Optional" + case .byte: "Optional" + case .char: "Optional" + case .short: "Optional" + case .int: "OptionalInt" + case .long: "OptionalLong" + case .float: "Optional" + case .double: "OptionalDouble" + case .javaLangString: "Optional" + default: nil + } + } + + var optionalWrapperType: String? { + switch self { + case .boolean, .byte, .char, .short, .float, .javaLangString: "Optional" + case .int: "OptionalInt" + case .long: "OptionalLong" + case .double: "OptionalDouble" + default: nil + } + } + + var optionalPlaceholderValue: String? { + switch self { + case .boolean: "false" + case .byte: "(byte) 0" + case .char: "(char) 0" + case .short: "(short) 0" + case .int: "0" + case .long: "0L" + case .float: "0f" + case .double: "0.0" + case .array, .class: "null" + case .void: nil + } + } + + var jniCallMethodAName: String { + switch self { + case .boolean: "CallBooleanMethodA" + case .byte: "CallByteMethodA" + case .char: "CallCharMethodA" + case .short: "CallShortMethodA" + case .int: "CallIntMethodA" + case .long: "CallLongMethodA" + case .float: "CallFloatMethodA" + case .double: "CallDoubleMethodA" + case .void: "CallVoidMethodA" + default: "CallObjectMethodA" + } + } + + /// Returns whether this type returns `JavaValue` from SwiftJava + var implementsJavaValue: Bool { + return switch self { + case .boolean, .byte, .char, .short, .int, .long, .float, .double, .void, .javaLangString: + true + default: + false + } + } + + /// Returns the boxed type, or self if the type is already a Java class. + var boxedType: JavaType { + switch self { + case .boolean: + return .class(package: "java.lang", name: "Boolean") + case .byte: + return .class(package: "java.lang", name: "Byte") + case .char: + return .class(package: "java.lang", name: "Character") + case .short: + return .class(package: "java.lang", name: "Short") + case .int: + return .class(package: "java.lang", name: "Integer") + case .long: + return .class(package: "java.lang", name: "Long") + case .float: + return .class(package: "java.lang", name: "Float") + case .double: + return .class(package: "java.lang", name: "Double") + case .void: + return .class(package: "java.lang", name: "Void") + default: + return self + } + } + + var requiresBoxing: Bool { + switch self { + case .boolean, .byte, .char, .short, .int, .long, .float, .double: + true + default: + false + } + } +} diff --git a/Sources/JExtractSwiftLib/Convenience/String+Extensions.swift b/Sources/JExtractSwiftLib/Convenience/String+Extensions.swift index 53781cefe..5641c7f23 100644 --- a/Sources/JExtractSwiftLib/Convenience/String+Extensions.swift +++ b/Sources/JExtractSwiftLib/Convenience/String+Extensions.swift @@ -12,14 +12,70 @@ // //===----------------------------------------------------------------------===// +import JavaTypes + extension String { - // TODO: naive implementation good enough for our simple case `methodMethodSomething` -> `MethodSomething` - var toCamelCase: String { + var firstCharacterUppercased: String { guard let f = first else { return self } return "\(f.uppercased())\(String(dropFirst()))" } -} \ No newline at end of file + + var firstCharacterLowercased: String { + guard let f = first else { + return self + } + + return "\(f.lowercased())\(String(dropFirst()))" + } + + /// Returns whether the string is of the format `isX` + var hasJavaBooleanNamingConvention: Bool { + guard self.hasPrefix("is"), self.count > 2 else { + return false + } + + let thirdCharacterIndex = self.index(self.startIndex, offsetBy: 2) + return self[thirdCharacterIndex].isUppercase + } + + /// Returns a version of the string correctly escaped for a JNI + var escapedJNIIdentifier: String { + self.map { + if $0 == "_" { + return "_1" + } else if $0 == "/" { + return "_" + } else if $0 == ";" { + return "_2" + } else if $0 == "[" { + return "_3" + } else if $0.isASCII && ($0.isLetter || $0.isNumber) { + return String($0) + } else if let utf16 = $0.utf16.first { + // Escape any non-alphanumeric to their UTF16 hex encoding + let utf16Hex = String(format: "%04x", utf16) + return "_0\(utf16Hex)" + } else { + fatalError("Invalid JNI character: \($0)") + } + } + .joined() + } + + /// Looks up self as a SwiftJava wrapped class name and converts it + /// into a `JavaType.class` if it exists in `lookupTable`. + func parseJavaClassFromSwiftJavaName(in lookupTable: [String: String]) -> JavaType? { + guard let canonicalJavaName = lookupTable[self] else { + return nil + } + let nameParts = canonicalJavaName.components(separatedBy: ".") + let javaPackageName = nameParts.dropLast().joined(separator: ".") + let javaClassName = nameParts.last! + + return .class(package: javaPackageName, name: javaClassName) + } +} diff --git a/Sources/JExtractSwiftLib/Convenience/SwiftSyntax+Extensions.swift b/Sources/JExtractSwiftLib/Convenience/SwiftSyntax+Extensions.swift index b732b9f89..c9f87b6dd 100644 --- a/Sources/JExtractSwiftLib/Convenience/SwiftSyntax+Extensions.swift +++ b/Sources/JExtractSwiftLib/Convenience/SwiftSyntax+Extensions.swift @@ -13,10 +13,10 @@ //===----------------------------------------------------------------------===// import SwiftDiagnostics -import SwiftSyntax +@_spi(ExperimentalLanguageFeatures) import SwiftSyntax extension WithModifiersSyntax { - internal var accessControlModifiers: DeclModifierListSyntax { + var accessControlModifiers: DeclModifierListSyntax { modifiers.filter { modifier in modifier.isAccessControl } @@ -24,7 +24,7 @@ extension WithModifiersSyntax { } extension ImplicitlyUnwrappedOptionalTypeSyntax { - internal var asOptionalTypeSyntax: any TypeSyntaxProtocol { + var asOptionalTypeSyntax: any TypeSyntaxProtocol { OptionalTypeSyntax( leadingTrivia: leadingTrivia, unexpectedBeforeWrappedType, @@ -51,27 +51,84 @@ extension DeclModifierSyntax { extension DeclModifierSyntax { var isPublic: Bool { switch self.name.tokenKind { - case .keyword(.private): return false - case .keyword(.fileprivate): return false - case .keyword(.internal): return false - case .keyword(.package): return false - case .keyword(.public): return true - case .keyword(.open): return true - default: return false + case .keyword(.private): false + case .keyword(.fileprivate): false + case .keyword(.internal): false + case .keyword(.package): false + case .keyword(.public): true + case .keyword(.open): true + default: false } } + + var isPackage: Bool { + switch self.name.tokenKind { + case .keyword(.private): false + case .keyword(.fileprivate): false + case .keyword(.internal): false + case .keyword(.package): true + case .keyword(.public): false + case .keyword(.open): false + default: false + } + } + + var isAtLeastPackage: Bool { + isPackage || isPublic + } + + var isInternal: Bool { + return switch self.name.tokenKind { + case .keyword(.private): false + case .keyword(.fileprivate): false + case .keyword(.internal): true + case .keyword(.package): false + case .keyword(.public): false + case .keyword(.open): false + default: false + } + } + + var isAtLeastInternal: Bool { + isInternal || isPackage || isPublic + } } extension WithModifiersSyntax { - var isPublic: Bool { - self.modifiers.contains { modifier in + func isPublic(in type: NominalTypeDeclSyntaxNode?) -> Bool { + if let type, case .protocolDecl(let protocolDecl) = Syntax(type).as(SyntaxEnum.self) { + return protocolDecl.isPublic(in: nil) + } + + return self.modifiers.contains { modifier in modifier.isPublic } } + + var isAtLeastPackage: Bool { + if self.modifiers.isEmpty { + return false + } + + return self.modifiers.contains { modifier in + modifier.isAtLeastInternal + } + } + + var isAtLeastInternal: Bool { + if self.modifiers.isEmpty { + // we assume that default access level is internal + return true + } + + return self.modifiers.contains { modifier in + modifier.isAtLeastInternal + } + } } extension AttributeListSyntax.Element { - /// Whether this node has `JavaKit` attributes. + /// Whether this node has `SwiftJava` attributes. var isJava: Bool { guard case let .attribute(attr) = self else { // FIXME: Handle #if. @@ -161,6 +218,8 @@ extension DeclSyntaxProtocol { } else { "var" } + case .usingDecl(let node): + node.nameForDebug } } @@ -200,6 +259,8 @@ extension DeclSyntaxProtocol { } )) .triviaSanitizedDescription + case .enumCaseDecl(let node): + node.triviaSanitizedDescription default: fatalError("unimplemented \(self.kind)") } diff --git a/Sources/JExtractSwiftLib/FFM/CDeclLowering/CRepresentation.swift b/Sources/JExtractSwiftLib/FFM/CDeclLowering/CRepresentation.swift index e1a69d12c..3f821a1b4 100644 --- a/Sources/JExtractSwiftLib/FFM/CDeclLowering/CRepresentation.swift +++ b/Sources/JExtractSwiftLib/FFM/CDeclLowering/CRepresentation.swift @@ -24,7 +24,7 @@ extension CType { init(cdeclType: SwiftType) throws { switch cdeclType { case .nominal(let nominalType): - if let knownType = nominalType.nominalTypeDecl.knownStandardLibraryType { + if let knownType = nominalType.nominalTypeDecl.knownTypeKind { if let primitiveCType = knownType.primitiveCType { self = primitiveCType return @@ -65,7 +65,10 @@ extension CType { case .tuple([]): self = .void - case .metatype, .optional, .tuple: + case .optional(let wrapped) where wrapped.isPointer: + try self.init(cdeclType: wrapped) + + case .genericParameter, .metatype, .optional, .tuple, .opaque, .existential, .composite, .array: throw CDeclToCLoweringError.invalidCDeclType(cdeclType) } } @@ -95,11 +98,11 @@ extension CFunction { enum CDeclToCLoweringError: Error { case invalidCDeclType(SwiftType) - case invalidNominalType(SwiftNominalTypeDeclaration) + case invalidNominalType(SwiftTypeDeclaration) case invalidFunctionConvention(SwiftFunctionType) } -extension KnownStandardLibraryType { +extension SwiftKnownTypeDeclKind { /// Determine the primitive C type that corresponds to this C standard /// library type, if there is one. var primitiveCType: CType? { @@ -122,7 +125,9 @@ extension KnownStandardLibraryType { .qualified(const: true, volatile: false, type: .void) ) case .void: .void - case .unsafePointer, .unsafeMutablePointer, .unsafeBufferPointer, .unsafeMutableBufferPointer, .string: + case .unsafePointer, .unsafeMutablePointer, .unsafeRawBufferPointer, .unsafeMutableRawBufferPointer, + .unsafeBufferPointer, .unsafeMutableBufferPointer, .string, .foundationData, .foundationDataProtocol, + .essentialsData, .essentialsDataProtocol, .optional, .array: nil } } diff --git a/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift b/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift index 02501dc53..a7da370b3 100644 --- a/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift +++ b/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift @@ -26,10 +26,10 @@ extension FFMSwift2JavaGenerator { ) throws -> LoweredFunctionSignature { let signature = try SwiftFunctionSignature( decl, - enclosingType: try enclosingType.map { try SwiftType($0, symbolTable: symbolTable) }, - symbolTable: symbolTable + enclosingType: try enclosingType.map { try SwiftType($0, lookupContext: lookupContext) }, + lookupContext: lookupContext ) - return try CdeclLowering(swiftStdlibTypes: swiftStdlibTypes).lowerFunctionSignature(signature) + return try CdeclLowering(symbolTable: lookupContext.symbolTable).lowerFunctionSignature(signature) } /// Lower the given initializer to a C-compatible entrypoint, @@ -42,11 +42,11 @@ extension FFMSwift2JavaGenerator { ) throws -> LoweredFunctionSignature { let signature = try SwiftFunctionSignature( decl, - enclosingType: try enclosingType.map { try SwiftType($0, symbolTable: symbolTable) }, - symbolTable: symbolTable + enclosingType: try enclosingType.map { try SwiftType($0, lookupContext: lookupContext) }, + lookupContext: lookupContext ) - return try CdeclLowering(swiftStdlibTypes: swiftStdlibTypes).lowerFunctionSignature(signature) + return try CdeclLowering(symbolTable: lookupContext.symbolTable).lowerFunctionSignature(signature) } /// Lower the given variable decl to a C-compatible entrypoint, @@ -66,16 +66,24 @@ extension FFMSwift2JavaGenerator { let signature = try SwiftFunctionSignature( decl, isSet: isSet, - enclosingType: try enclosingType.map { try SwiftType($0, symbolTable: symbolTable) }, - symbolTable: symbolTable + enclosingType: try enclosingType.map { try SwiftType($0, lookupContext: lookupContext) }, + lookupContext: lookupContext ) - return try CdeclLowering(swiftStdlibTypes: swiftStdlibTypes).lowerFunctionSignature(signature) + return try CdeclLowering(symbolTable: lookupContext.symbolTable).lowerFunctionSignature(signature) } } /// Responsible for lowering Swift API to C API. struct CdeclLowering { - var swiftStdlibTypes: SwiftStandardLibraryTypes + var knownTypes: SwiftKnownTypes + + init(knownTypes: SwiftKnownTypes) { + self.knownTypes = knownTypes + } + + init(symbolTable: SwiftSymbolTable) { + self.knownTypes = SwiftKnownTypes(symbolTable: symbolTable) + } /// Lower the given Swift function signature to a Swift @_cdecl function signature, /// which is C compatible, and the corresponding Java method signature. @@ -90,7 +98,9 @@ struct CdeclLowering { try lowerParameter( selfParameter.type, convention: selfParameter.convention, - parameterName: selfParameter.parameterName ?? "self" + parameterName: selfParameter.parameterName ?? "self", + genericParameters: signature.genericParameters, + genericRequirements: signature.genericRequirements ) case nil, .initializer(_), .staticMethod(_): nil @@ -98,13 +108,20 @@ struct CdeclLowering { // Lower all of the parameters. let loweredParameters = try signature.parameters.enumerated().map { (index, param) in - try lowerParameter( + return try lowerParameter( param.type, convention: param.convention, - parameterName: param.parameterName ?? "_\(index)" + parameterName: param.parameterName ?? "_\(index)", + genericParameters: signature.genericParameters, + genericRequirements: signature.genericRequirements ) } + for effect in signature.effectSpecifiers { + // Prohibit any effects for now. + throw LoweringError.effectNotSupported(effect) + } + // Lower the result. let loweredResult = try lowerResult(signature.result.type) @@ -129,7 +146,9 @@ struct CdeclLowering { func lowerParameter( _ type: SwiftType, convention: SwiftParameterConvention, - parameterName: String + parameterName: String, + genericParameters: [SwiftGenericParameterDeclaration], + genericRequirements: [SwiftGenericRequirement] ) throws -> LoweredParameter { // If there is a 1:1 mapping between this Swift type and a C type, we just // return it. @@ -149,18 +168,14 @@ struct CdeclLowering { SwiftParameter( convention: .byValue, parameterName: parameterName, - type: .nominal( - SwiftNominalType( - nominalTypeDecl: swiftStdlibTypes[.unsafeRawPointer] - ) - ) + type: knownTypes.unsafeRawPointer ) ], conversion: .unsafeCastPointer(.placeholder, swiftType: instanceType) ) case .nominal(let nominal): - if let knownType = nominal.nominalTypeDecl.knownStandardLibraryType { + if let knownType = nominal.nominalTypeDecl.knownTypeKind { if convention == .inout { // FIXME: Support non-trivial 'inout' for builtin types. throw LoweringError.inoutNotSupported(type) @@ -173,30 +188,32 @@ struct CdeclLowering { // Typed pointers are mapped down to their raw forms in cdecl entry // points. These can be passed through directly. let isMutable = knownType == .unsafeMutablePointer - let rawPointerNominal = swiftStdlibTypes[isMutable ? .unsafeMutableRawPointer : .unsafeRawPointer] - let paramType: SwiftType = .nominal(SwiftNominalType(nominalTypeDecl: rawPointerNominal)) return LoweredParameter( - cdeclParameters: [SwiftParameter(convention: .byValue, parameterName: parameterName, type: paramType)], + cdeclParameters: [ + SwiftParameter( + convention: .byValue, + parameterName: parameterName, + type: isMutable ? knownTypes.unsafeMutableRawPointer : knownTypes.unsafeRawPointer + ) + ], conversion: .typedPointer(.placeholder, swiftType: genericArgs[0]) ) case .unsafeBufferPointer, .unsafeMutableBufferPointer: - guard let genericArgs = type.asNominalType?.genericArguments, genericArgs.count == 1 else { + guard let genericArgs = nominal.genericArguments, genericArgs.count == 1 else { throw LoweringError.unhandledType(type) } // Typed pointers are lowered to (raw-pointer, count) pair. let isMutable = knownType == .unsafeMutableBufferPointer - let rawPointerNominal = swiftStdlibTypes[isMutable ? .unsafeMutableRawPointer : .unsafeRawPointer] - return LoweredParameter( cdeclParameters: [ SwiftParameter( convention: .byValue, parameterName: "\(parameterName)_pointer", - type: .nominal(SwiftNominalType(nominalTypeDecl: rawPointerNominal)) + type: isMutable ? knownTypes.unsafeMutableRawPointer : knownTypes.unsafeRawPointer ), SwiftParameter( convention: .byValue, parameterName: "\(parameterName)_count", - type: .nominal(SwiftNominalType(nominalTypeDecl: swiftStdlibTypes[.int])) + type: knownTypes.int ), ], conversion: .initialize( type, @@ -213,6 +230,41 @@ struct CdeclLowering { ) ) + case .unsafeRawBufferPointer, .unsafeMutableRawBufferPointer: + // pointer buffers are lowered to (raw-pointer, count) pair. + let isMutable = knownType == .unsafeMutableRawBufferPointer + return LoweredParameter( + cdeclParameters: [ + SwiftParameter( + convention: .byValue, + parameterName: "\(parameterName)_pointer", + type: .optional(isMutable ? knownTypes.unsafeMutableRawPointer : knownTypes.unsafeRawPointer) + ), + SwiftParameter( + convention: .byValue, parameterName: "\(parameterName)_count", + type: knownTypes.int + ) + ], + conversion: .initialize( + type, + arguments: [ + LabeledArgument( + label: "start", + argument: .explodedComponent(.placeholder, component: "pointer") + ), + LabeledArgument( + label: "count", + argument: .explodedComponent(.placeholder, component: "count") + ) + ] + )) + + case .optional: + guard let genericArgs = nominal.genericArguments, genericArgs.count == 1 else { + throw LoweringError.unhandledType(type) + } + return try lowerOptionalParameter(genericArgs[0], convention: convention, parameterName: parameterName, genericParameters: genericParameters, genericRequirements: genericRequirements) + case .string: // 'String' is passed in by C string. i.e. 'UnsafePointer' ('const uint8_t *') if knownType == .string { @@ -221,12 +273,7 @@ struct CdeclLowering { SwiftParameter( convention: .byValue, parameterName: parameterName, - type: .nominal(SwiftNominalType( - nominalTypeDecl: swiftStdlibTypes.unsafePointerDecl, - genericArguments: [ - .nominal(SwiftNominalType(nominalTypeDecl: swiftStdlibTypes[.int8])) - ] - )) + type: knownTypes.unsafePointer(knownTypes.int8) ) ], conversion: .initialize(type, arguments: [ @@ -235,6 +282,9 @@ struct CdeclLowering { ) } + case .foundationData, .essentialsData: + break + default: // Unreachable? Should be handled by `CType(cdeclType:)` lowering above. throw LoweringError.unhandledType(type) @@ -243,15 +293,12 @@ struct CdeclLowering { // Arbitrary nominal types are passed-in as an pointer. let isMutable = (convention == .inout) - let rawPointerNominal = swiftStdlibTypes[isMutable ? .unsafeMutableRawPointer : .unsafeRawPointer] - let parameterType: SwiftType = .nominal(SwiftNominalType(nominalTypeDecl: rawPointerNominal)) - return LoweredParameter( cdeclParameters: [ SwiftParameter( convention: .byValue, parameterName: parameterName, - type: parameterType + type: isMutable ? knownTypes.unsafeMutableRawPointer : knownTypes.unsafeRawPointer ), ], conversion: .pointee(.typedPointer(.placeholder, swiftType: type)) @@ -259,7 +306,7 @@ struct CdeclLowering { case .tuple(let tuple): if tuple.count == 1 { - return try lowerParameter(tuple[0], convention: convention, parameterName: parameterName) + return try lowerParameter(tuple[0], convention: convention, parameterName: parameterName, genericParameters: genericParameters, genericRequirements: genericRequirements) } if convention == .inout { throw LoweringError.inoutNotSupported(type) @@ -269,7 +316,7 @@ struct CdeclLowering { for (idx, element) in tuple.enumerated() { // FIXME: Use tuple element label. let cdeclName = "\(parameterName)_\(idx)" - let lowered = try lowerParameter(element, convention: convention, parameterName: cdeclName) + let lowered = try lowerParameter(element, convention: convention, parameterName: cdeclName, genericParameters: genericParameters, genericRequirements: genericRequirements) parameters.append(contentsOf: lowered.cdeclParameters) conversions.append(lowered.conversion) @@ -289,11 +336,93 @@ struct CdeclLowering { conversion: conversion ) - case .optional: + case .opaque, .existential, .genericParameter: + if let concreteTy = type.representativeConcreteTypeIn(knownTypes: knownTypes, genericParameters: genericParameters, genericRequirements: genericRequirements) { + return try lowerParameter(concreteTy, convention: convention, parameterName: parameterName, genericParameters: genericParameters, genericRequirements: genericRequirements) + } + throw LoweringError.unhandledType(type) + + case .optional(let wrapped): + return try lowerOptionalParameter(wrapped, convention: convention, parameterName: parameterName, genericParameters: genericParameters, genericRequirements: genericRequirements) + + case .composite: + throw LoweringError.unhandledType(type) + + case .array: throw LoweringError.unhandledType(type) } } + /// Lower a Swift Optional to cdecl function type. + /// + /// - Parameters: + /// - fn: the Swift function type to lower. + func lowerOptionalParameter( + _ wrappedType: SwiftType, + convention: SwiftParameterConvention, + parameterName: String, + genericParameters: [SwiftGenericParameterDeclaration], + genericRequirements: [SwiftGenericRequirement] + ) throws -> LoweredParameter { + // If there is a 1:1 mapping between this Swift type and a C type, lower it to 'UnsafePointer?' + if let _ = try? CType(cdeclType: wrappedType) { + return LoweredParameter( + cdeclParameters: [ + SwiftParameter(convention: .byValue, parameterName: parameterName, type: .optional(knownTypes.unsafePointer(wrappedType))) + ], + conversion: .pointee(.optionalChain(.placeholder)) + ) + } + + switch wrappedType { + case .nominal(let nominal): + if let knownType = nominal.nominalTypeDecl.knownTypeKind { + switch knownType { + case .foundationData, .essentialsData: + break + case .unsafeRawPointer, .unsafeMutableRawPointer: + throw LoweringError.unhandledType(.optional(wrappedType)) + case .unsafeRawBufferPointer, .unsafeMutableRawBufferPointer: + throw LoweringError.unhandledType(.optional(wrappedType)) + case .unsafePointer, .unsafeMutablePointer: + throw LoweringError.unhandledType(.optional(wrappedType)) + case .unsafeBufferPointer, .unsafeMutableBufferPointer: + throw LoweringError.unhandledType(.optional(wrappedType)) + case .void, .string: + throw LoweringError.unhandledType(.optional(wrappedType)) + case .foundationDataProtocol, .essentialsDataProtocol: + throw LoweringError.unhandledType(.optional(wrappedType)) + default: + // Unreachable? Should be handled by `CType(cdeclType:)` lowering above. + throw LoweringError.unhandledType(.optional(wrappedType)) + } + } + + // Lower arbitrary nominal to `UnsafeRawPointer?` + return LoweredParameter( + cdeclParameters: [ + SwiftParameter(convention: .byValue, parameterName: parameterName, type: .optional(knownTypes.unsafeRawPointer)) + ], + conversion: .pointee(.typedPointer(.optionalChain(.placeholder), swiftType: wrappedType)) + ) + + case .existential, .opaque, .genericParameter: + if let concreteTy = wrappedType.representativeConcreteTypeIn(knownTypes: knownTypes, genericParameters: genericParameters, genericRequirements: genericRequirements) { + return try lowerOptionalParameter(concreteTy, convention: convention, parameterName: parameterName, genericParameters: genericParameters, genericRequirements: genericRequirements) + } + throw LoweringError.unhandledType(.optional(wrappedType)) + + case .tuple(let tuple): + if tuple.count == 1 { + return try lowerOptionalParameter(tuple[0], convention: convention, parameterName: parameterName, genericParameters: genericParameters, genericRequirements: genericRequirements) + } + throw LoweringError.unhandledType(.optional(wrappedType)) + + case .function, .metatype, .optional, .composite, .array: + throw LoweringError.unhandledType(.optional(wrappedType)) + } + } + /// Lower a Swift function type (i.e. closure) to cdecl function type. /// /// - Parameters: @@ -304,14 +433,15 @@ struct CdeclLowering { var parameters: [SwiftParameter] = [] var parameterConversions: [ConversionStep] = [] - for parameter in fn.parameters { - if let _ = try? CType(cdeclType: parameter.type) { - parameters.append(SwiftParameter(convention: .byValue, type: parameter.type)) - parameterConversions.append(.placeholder) - } else { - // Non-trivial types are not yet supported. - throw LoweringError.unhandledType(.function(fn)) - } + for (i, parameter) in fn.parameters.enumerated() { + let parameterName = parameter.parameterName ?? "_\(i)" + let loweredParam = try lowerClosureParameter( + parameter.type, + convention: parameter.convention, + parameterName: parameterName + ) + parameters.append(contentsOf: loweredParam.cdeclParameters) + parameterConversions.append(loweredParam.conversion) } let resultType: SwiftType @@ -324,15 +454,77 @@ struct CdeclLowering { throw LoweringError.unhandledType(.function(fn)) } - // Ignore the conversions for now, since we don't support non-trivial types yet. - _ = (parameterConversions, resultConversion) + let isCompatibleWithC = parameterConversions.allSatisfy(\.isPlaceholder) && resultConversion.isPlaceholder return ( type: .function(SwiftFunctionType(convention: .c, parameters: parameters, resultType: resultType)), - conversion: .placeholder + conversion: isCompatibleWithC ? .placeholder : .closureLowering(parameters: parameterConversions, result: resultConversion) ) } + func lowerClosureParameter( + _ type: SwiftType, + convention: SwiftParameterConvention, + parameterName: String + ) throws -> LoweredParameter { + // If there is a 1:1 mapping between this Swift type and a C type, we just + // return it. + if let _ = try? CType(cdeclType: type) { + return LoweredParameter( + cdeclParameters: [ + SwiftParameter( + convention: .byValue, + parameterName: parameterName, + type: type + ), + ], + conversion: .placeholder + ) + } + + switch type { + case .nominal(let nominal): + if let knownType = nominal.nominalTypeDecl.knownTypeKind { + switch knownType { + case .unsafeRawBufferPointer, .unsafeMutableRawBufferPointer: + // pointer buffers are lowered to (raw-pointer, count) pair. + let isMutable = knownType == .unsafeMutableRawBufferPointer + return LoweredParameter( + cdeclParameters: [ + SwiftParameter( + convention: .byValue, + parameterName: "\(parameterName)_pointer", + type: .optional(isMutable ? knownTypes.unsafeMutableRawPointer : knownTypes.unsafeRawPointer) + ), + SwiftParameter( + convention: .byValue, + parameterName: "\(parameterName)_count", + type: knownTypes.int + ), + ], + conversion: .tuplify([ + .member(.placeholder, member: "baseAddress"), + .member(.placeholder, member: "count") + ]) + ) + + case .foundationData, .essentialsData: + break + + default: + throw LoweringError.unhandledType(type) + } + } + + // Custom types are not supported yet. + throw LoweringError.unhandledType(type) + + case .genericParameter, .function, .metatype, .optional, .tuple, .existential, .opaque, .composite, .array: + // TODO: Implement + throw LoweringError.unhandledType(type) + } + } + /// Lower a Swift result type to cdecl out parameters and return type. /// /// - Parameters: @@ -352,23 +544,20 @@ struct CdeclLowering { switch type { case .metatype: // 'unsafeBitcast(, to: UnsafeRawPointer.self)' as 'UnsafeRawPointer' - - let resultType: SwiftType = .nominal(SwiftNominalType(nominalTypeDecl: swiftStdlibTypes[.unsafeRawPointer])) return LoweredResult( - cdeclResultType: resultType, + cdeclResultType: knownTypes.unsafeRawPointer, cdeclOutParameters: [], - conversion: .unsafeCastPointer(.placeholder, swiftType: resultType) + conversion: .unsafeCastPointer(.placeholder, swiftType: knownTypes.unsafeRawPointer) ) case .nominal(let nominal): // Types from the Swift standard library that we know about. - if let knownType = nominal.nominalTypeDecl.knownStandardLibraryType { + if let knownType = nominal.nominalTypeDecl.knownTypeKind { switch knownType { case .unsafePointer, .unsafeMutablePointer: // Typed pointers are lowered to corresponding raw forms. let isMutable = knownType == .unsafeMutablePointer - let rawPointerNominal = swiftStdlibTypes[isMutable ? .unsafeMutableRawPointer : .unsafeRawPointer] - let resultType: SwiftType = .nominal(SwiftNominalType(nominalTypeDecl: rawPointerNominal)) + let resultType: SwiftType = isMutable ? knownTypes.unsafeMutableRawPointer : knownTypes.unsafeRawPointer return LoweredResult( cdeclResultType: resultType, cdeclOutParameters: [], @@ -378,20 +567,53 @@ struct CdeclLowering { case .unsafeBufferPointer, .unsafeMutableBufferPointer: // Typed pointers are lowered to (raw-pointer, count) pair. let isMutable = knownType == .unsafeMutableBufferPointer - let rawPointerType = swiftStdlibTypes[isMutable ? .unsafeMutableRawPointer : .unsafeRawPointer] return try lowerResult( .tuple([ - .nominal(SwiftNominalType(nominalTypeDecl: rawPointerType)), - .nominal(SwiftNominalType(nominalTypeDecl: swiftStdlibTypes[.int])) + isMutable ? knownTypes.unsafeMutableRawPointer : knownTypes.unsafeRawPointer, + knownTypes.int ]), outParameterName: outParameterName ) + case .unsafeRawBufferPointer, .unsafeMutableRawBufferPointer: + // pointer buffers are lowered to (raw-pointer, count) pair. + let isMutable = knownType == .unsafeMutableRawBufferPointer + return LoweredResult( + cdeclResultType: .void, + cdeclOutParameters: [ + SwiftParameter( + convention: .byValue, + parameterName: "\(outParameterName)_pointer", + type: knownTypes.unsafeMutablePointer( + .optional(isMutable ? knownTypes.unsafeMutableRawPointer : knownTypes.unsafeRawPointer) + ) + ), + SwiftParameter( + convention: .byValue, + parameterName: "\(outParameterName)_count", + type: knownTypes.unsafeMutablePointer(knownTypes.int) + ), + ], + conversion: .aggregate([ + .populatePointer( + name: "\(outParameterName)_pointer", + to: .member(.placeholder, member: "baseAddress") + ), + .populatePointer( + name: "\(outParameterName)_count", + to: .member(.placeholder, member: "count") + ) + ], name: outParameterName) + ) + case .void: return LoweredResult(cdeclResultType: .void, cdeclOutParameters: [], conversion: .placeholder) - case .string: - // Returning string is not supported at this point. + case .foundationData, .essentialsData: + break + + case .string, .optional: + // Not supported at this point. throw LoweringError.unhandledType(type) default: @@ -407,7 +629,7 @@ struct CdeclLowering { SwiftParameter( convention: .byValue, parameterName: outParameterName, - type: .nominal(SwiftNominalType(nominalTypeDecl: swiftStdlibTypes[.unsafeMutableRawPointer])) + type: knownTypes.unsafeMutableRawPointer ) ], conversion: .populatePointer(name: outParameterName, assumingType: type, to: .placeholder) @@ -431,10 +653,7 @@ struct CdeclLowering { let parameter = SwiftParameter( convention: .byValue, parameterName: parameterName, - type: .nominal(SwiftNominalType( - nominalTypeDecl: swiftStdlibTypes.unsafeMutablePointerDecl, - genericArguments: [lowered.cdeclResultType] - )) + type: knownTypes.unsafeMutablePointer(lowered.cdeclResultType) ) parameters.append(parameter) conversions.append(.populatePointer( @@ -454,7 +673,7 @@ struct CdeclLowering { conversion: .tupleExplode(conversions, name: outParameterName) ) - case .function(_), .optional(_): + case .genericParameter, .function, .optional, .existential, .opaque, .composite, .array: throw LoweringError.unhandledType(type) } } @@ -539,7 +758,10 @@ public struct LoweredFunctionSignature: Equatable { SwiftFunctionSignature( selfParameter: nil, parameters: allLoweredParameters, - result: SwiftResult(convention: .direct, type: result.cdeclResultType) + result: SwiftResult(convention: .direct, type: result.cdeclResultType), + effectSpecifiers: [], + genericParameters: [], + genericRequirements: [] ) } } @@ -550,8 +772,7 @@ extension LoweredFunctionSignature { package func cdeclThunk( cName: String, swiftAPIName: String, - as apiKind: SwiftAPIKind, - stdlibTypes: SwiftStandardLibraryTypes + as apiKind: SwiftAPIKind ) -> FunctionDeclSyntax { let cdeclParams = allLoweredParameters.map(\.description).joined(separator: ", ") @@ -591,11 +812,10 @@ extension LoweredFunctionSignature { // Build callee expression. let callee: ExprSyntax = if let selfExpr { - if case .initializer = apiKind { + switch apiKind { // Don't bother to create explicit ${Self}.init expression. - selfExpr - } else { - ExprSyntax(MemberAccessExprSyntax(base: selfExpr, name: .identifier(swiftAPIName))) + case .initializer, .subscriptGetter, .subscriptSetter: selfExpr + default: ExprSyntax(MemberAccessExprSyntax(base: selfExpr, name: .identifier(swiftAPIName))) } } else { ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(swiftAPIName))) @@ -620,6 +840,22 @@ extension LoweredFunctionSignature { case .setter: assert(paramExprs.count == 1) resultExpr = "\(callee) = \(paramExprs[0])" + + case .enumCase: + // This should not be called, but let's fatalError. + fatalError("Enum cases are not supported with FFM.") + + case .subscriptGetter: + let parameters = paramExprs.map { $0.description }.joined(separator: ", ") + resultExpr = "\(callee)[\(raw: parameters)]" + case .subscriptSetter: + assert(paramExprs.count >= 1) + + var argumentsWithoutNewValue = paramExprs + let newValueArgument = argumentsWithoutNewValue.removeLast() + + let parameters = argumentsWithoutNewValue.map { $0.description }.joined(separator: ", ") + resultExpr = "\(callee)[\(raw: parameters)] = \(newValueArgument)" } // Lower the result. @@ -652,6 +888,7 @@ extension LoweredFunctionSignature { } enum LoweringError: Error { - case inoutNotSupported(SwiftType) - case unhandledType(SwiftType) + case inoutNotSupported(SwiftType, file: String = #file, line: Int = #line) + case unhandledType(SwiftType, file: String = #file, line: Int = #line) + case effectNotSupported(SwiftEffectSpecifier, file: String = #file, line: Int = #line) } diff --git a/Sources/JExtractSwiftLib/FFM/ConversionStep.swift b/Sources/JExtractSwiftLib/FFM/ConversionStep.swift index 31d731bc2..f59f739dd 100644 --- a/Sources/JExtractSwiftLib/FFM/ConversionStep.swift +++ b/Sources/JExtractSwiftLib/FFM/ConversionStep.swift @@ -52,9 +52,18 @@ enum ConversionStep: Equatable { /// Initialize mutable raw pointer with a typed value. indirect case populatePointer(name: String, assumingType: SwiftType? = nil, to: ConversionStep) - /// Perform multiple conversions, but discard the result. + /// Perform multiple conversions for each tuple input elements, but discard the result. case tupleExplode([ConversionStep], name: String?) + /// Perform multiple conversions using the same input. + case aggregate([ConversionStep], name: String?) + + indirect case closureLowering(parameters: [ConversionStep], result: ConversionStep) + + indirect case member(ConversionStep, member: String) + + indirect case optionalChain(ConversionStep) + /// Count the number of times that the placeholder occurs within this /// conversion step. var placeholderCount: Int { @@ -63,17 +72,25 @@ enum ConversionStep: Equatable { .pointee(let inner), .typedPointer(let inner, swiftType: _), .unsafeCastPointer(let inner, swiftType: _), - .populatePointer(name: _, assumingType: _, to: let inner): + .populatePointer(name: _, assumingType: _, to: let inner), + .member(let inner, member: _), .optionalChain(let inner): inner.placeholderCount case .initialize(_, arguments: let arguments): arguments.reduce(0) { $0 + $1.argument.placeholderCount } - case .placeholder, .tupleExplode: + case .placeholder, .tupleExplode, .closureLowering: 1 - case .tuplify(let elements): + case .tuplify(let elements), .aggregate(let elements, _): elements.reduce(0) { $0 + $1.placeholderCount } } } + var isPlaceholder: Bool { + if case .placeholder = self { + return true + } + return false + } + /// Convert the conversion step into an expression with the given /// value as the placeholder value in the expression. func asExprSyntax(placeholder: String, bodyItems: inout [CodeBlockItemSyntax]) -> ExprSyntax? { @@ -106,7 +123,7 @@ enum ConversionStep: Equatable { // of splatting out text. let renderedArgumentList = renderedArguments.joined(separator: ", ") return "\(raw: type.description)(\(raw: renderedArgumentList))" - + case .tuplify(let elements): let renderedElements: [String] = elements.enumerated().map { (index, element) in element.asExprSyntax(placeholder: "\(placeholder)_\(index)", bodyItems: &bodyItems)!.description @@ -140,6 +157,71 @@ enum ConversionStep: Equatable { } } return nil + + case .member(let step, let member): + let inner = step.asExprSyntax(placeholder: placeholder, bodyItems: &bodyItems) + return "\(inner).\(raw: member)" + + case .aggregate(let steps, let name): + let toExplode: String + if let name { + bodyItems.append("let \(raw: name) = \(raw: placeholder)") + toExplode = name + } else { + toExplode = placeholder + } + for step in steps { + if let result = step.asExprSyntax(placeholder: toExplode, bodyItems: &bodyItems) { + bodyItems.append(CodeBlockItemSyntax(item: .expr(result))) + } + } + return nil + + case .optionalChain(let step): + let inner = step.asExprSyntax(placeholder: placeholder, bodyItems: &bodyItems) + return ExprSyntax(OptionalChainingExprSyntax(expression: inner!)) + + case .closureLowering(let parameterSteps, let resultStep): + var body: [CodeBlockItemSyntax] = [] + + // Lower parameters. + var params: [String] = [] + var args: [ExprSyntax] = [] + for (i, parameterStep) in parameterSteps.enumerated() { + let paramName = "_\(i)" + params.append(paramName) + if case .tuplify(let elemSteps) = parameterStep { + for elemStep in elemSteps { + if let elemExpr = elemStep.asExprSyntax(placeholder: paramName, bodyItems: &body) { + args.append(elemExpr) + } + } + } else if let paramExpr = parameterStep.asExprSyntax(placeholder: paramName, bodyItems: &body) { + args.append(paramExpr) + } + } + + // Call the lowered closure with lowered parameters. + let loweredResult = "\(placeholder)(\(args.map(\.description).joined(separator: ", ")))" + + // Raise the lowered result. + let result = resultStep.asExprSyntax(placeholder: loweredResult.description, bodyItems: &body) + body.append("return \(result)") + + // Construct the closure expression. + var closure = ExprSyntax( + """ + { (\(raw: params.joined(separator: ", "))) in + } + """ + ).cast(ClosureExprSyntax.self) + + closure.statements = CodeBlockItemListSyntax { + body.map { + $0.with(\.leadingTrivia, [.newlines(1), .spaces(4)]) + } + } + return ExprSyntax(closure) } } } diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift index c7c6631ac..a4485fff0 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift @@ -105,9 +105,16 @@ extension FFMSwift2JavaGenerator { var params: [String] = [] var args: [String] = [] for param in cFunc.parameters { - // ! unwrapping because cdecl lowering guarantees the parameter named. - params.append("\(param.type.javaType) \(param.name!)") - args.append(param.name!) + let name = param.name! // !-safe, because cdecl lowering guarantees the parameter named. + + let annotationsStr = + if param.type.javaType.parameterAnnotations.isEmpty { + "" + } else { + param.type.javaType.parameterAnnotations.map({$0.render()}).joined(separator: " ") + " " + } + params.append("\(annotationsStr)\(param.type.javaType) \(name)") + args.append(name) } let paramsStr = params.joined(separator: ", ") let argsStr = args.joined(separator: ", ") @@ -116,8 +123,8 @@ extension FFMSwift2JavaGenerator { """ public static \(returnTy) call(\(paramsStr)) { try { - if (SwiftKit.TRACE_DOWNCALLS) { - SwiftKit.traceDowncall(\(argsStr)); + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall(\(argsStr)); } \(maybeReturn)HANDLE.invokeExact(\(argsStr)); } catch (Throwable ex$) { @@ -156,7 +163,7 @@ extension FFMSwift2JavaGenerator { /// apply(); /// } /// static final MethodDescriptor DESC = FunctionDescriptor.of(...); - /// static final MethodHandle HANDLE = SwiftKit.upcallHandle(Function.class, "apply", DESC); + /// static final MethodHandle HANDLE = SwiftRuntime.upcallHandle(Function.class, "apply", DESC); /// static MemorySegment toUpcallStub(Function fi, Arena arena) { /// return Linker.nativeLinker().upcallStub(HANDLE.bindTo(fi), DESC, arena); /// } @@ -197,7 +204,7 @@ extension FFMSwift2JavaGenerator { printFunctionDescriptorDefinition(&printer, cResultType, cParams) printer.print( """ - private static final MethodHandle HANDLE = SwiftKit.upcallHandle(Function.class, "apply", DESC); + private static final MethodHandle HANDLE = SwiftRuntime.upcallHandle(Function.class, "apply", DESC); private static MemorySegment toUpcallStub(Function fi, Arena arena) { return Linker.nativeLinker().upcallStub(HANDLE.bindTo(fi), DESC, arena); } @@ -251,7 +258,6 @@ extension FFMSwift2JavaGenerator { ) } else { // Otherwise, the lambda must be wrapped with the lowered function instance. - assertionFailure("should be unreachable at this point") let apiParams = functionType.parameters.flatMap { $0.javaParameters.map { param in "\(param.type) \(param.name)" } } @@ -262,13 +268,38 @@ extension FFMSwift2JavaGenerator { public interface \(functionType.name) { \(functionType.result.javaResultType) apply(\(apiParams.joined(separator: ", "))); } - private static MemorySegment $toUpcallStub(\(functionType.name) fi, Arena arena) { - return \(cdeclDescriptor).toUpcallStub(() -> { - fi() - }, arena); - } """ ) + + let cdeclParams = functionType.cdeclType.parameters.map( { "\($0.parameterName!)" }) + + printer.printBraceBlock( + """ + private static MemorySegment $toUpcallStub(\(functionType.name) fi, Arena arena) + """ + ) { printer in + printer.print( + """ + return \(cdeclDescriptor).toUpcallStub((\(cdeclParams.joined(separator: ", "))) -> { + """ + ) + printer.indent() + var convertedArgs: [String] = [] + for param in functionType.parameters { + let arg = param.conversion.render(&printer, param.javaParameters[0].name) + convertedArgs.append(arg) + } + + let call = "fi.apply(\(convertedArgs.joined(separator: ", ")))" + let result = functionType.result.conversion.render(&printer, call) + if functionType.result.javaResultType == .void { + printer.print("\(result);") + } else { + printer.print("return \(result);") + } + printer.outdent() + printer.print("}, arena);") + } } } @@ -292,23 +323,21 @@ extension FFMSwift2JavaGenerator { let translatedSignature = translated.translatedSignature let returnTy = translatedSignature.result.javaResultType + var annotationsStr = translatedSignature.annotations.map({ $0.render() }).joined(separator: "\n") + if !annotationsStr.isEmpty { annotationsStr += "\n" } + var paramDecls = translatedSignature.parameters .flatMap(\.javaParameters) - .map { "\($0.type) \($0.name)" } + .map { $0.renderParameter() } if translatedSignature.requiresSwiftArena { - paramDecls.append("SwiftArena swiftArena$") + paramDecls.append("AllocatingSwiftArena swiftArena$") } // TODO: we could copy the Swift method's documentation over here, that'd be great UX + printDeclDocumentation(&printer, decl) printer.printBraceBlock( """ - /** - * Downcall to Swift: - * {@snippet lang=swift : - * \(decl.signatureString) - * } - */ - \(modifiers) \(returnTy) \(methodName)(\(paramDecls.joined(separator: ", "))) + \(annotationsStr)\(modifiers) \(returnTy) \(methodName)(\(paramDecls.joined(separator: ", "))) """ ) { printer in if case .instance(_) = decl.functionSignature.selfParameter { @@ -320,6 +349,19 @@ extension FFMSwift2JavaGenerator { } } + private func printDeclDocumentation(_ printer: inout CodePrinter, _ decl: ImportedFunc) { + printer.print( + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * \(decl.signatureString) + * } + */ + """ + ) + } + /// Print the actual downcall to the Swift API. /// /// This assumes that all the parameters are passed-in with appropriate names. @@ -354,9 +396,12 @@ extension FFMSwift2JavaGenerator { // Indirect return receivers. for outParameter in translatedSignature.result.outParameters { - let memoryLayout = renderMemoryLayoutValue(for: outParameter.type) + guard case .concrete(let type) = outParameter.type else { + continue + } + let memoryLayout = renderMemoryLayoutValue(for: type) - let arena = if let className = outParameter.type.className, + let arena = if let className = type.className, analysis.importedTypes[className] != nil { // Use passed-in 'SwiftArena' for 'SwiftValue'. "swiftArena$" @@ -365,7 +410,8 @@ extension FFMSwift2JavaGenerator { "arena$" } - let varName = "_result" + outParameter.name + // FIXME: use trailing$ convention + let varName = outParameter.name.isEmpty ? "_result" : "_result_" + outParameter.name printer.print( "MemorySegment \(varName) = \(arena).allocate(\(memoryLayout));" @@ -407,7 +453,7 @@ extension FFMSwift2JavaGenerator { func renderMemoryLayoutValue(for javaType: JavaType) -> String { if let layout = ForeignValueLayout(javaType: javaType) { return layout.description - } else if case .class(package: _, name: let customClass) = javaType { + } else if case .class(package: _, name: let customClass, _) = javaType { return ForeignValueLayout(customType: customClass).description } else { fatalError("renderMemoryLayoutValue not supported for \(javaType)") @@ -415,36 +461,43 @@ extension FFMSwift2JavaGenerator { } } -extension JavaConversionStep { +extension FFMSwift2JavaGenerator.JavaConversionStep { /// Whether the conversion uses SwiftArena. var requiresSwiftArena: Bool { switch self { - case .pass, .swiftValueSelfSegment, .construct, .cast, .call, .method: + case .placeholder, .explodedName, .constant, .readMemorySegment: return false - case .constructSwiftValue: + case .constructSwiftValue, .wrapMemoryAddressUnsafe: return true + + case .call(let inner, _, _), .cast(let inner, _), .construct(let inner, _), + .method(let inner, _, _, _), .swiftValueSelfSegment(let inner): + return inner.requiresSwiftArena + + case .commaSeparated(let list): + return list.contains(where: { $0.requiresSwiftArena }) } } /// Whether the conversion uses temporary Arena. var requiresTemporaryArena: Bool { switch self { - case .pass, .swiftValueSelfSegment, .construct, .constructSwiftValue, .cast: + case .placeholder, .explodedName, .constant: return false - case .call(_, let withArena), .method(_, _, let withArena): - return withArena - } - } - - /// Whether if the result evaluation is trivial. - /// - /// If this is false, it's advised to store it to a variable if it's used multiple times - var isTrivial: Bool { - switch self { - case .pass, .swiftValueSelfSegment: + case .readMemorySegment: return true - case .cast, .construct, .constructSwiftValue, .call, .method: - return false + case .cast(let inner, _), + .construct(let inner, _), + .constructSwiftValue(let inner, _), + .swiftValueSelfSegment(let inner), + .wrapMemoryAddressUnsafe(let inner, _): + return inner.requiresSwiftArena + case .call(let inner, _, let withArena): + return withArena || inner.requiresTemporaryArena + case .method(let inner, _, let args, let withArena): + return withArena || inner.requiresTemporaryArena || args.contains(where: { $0.requiresTemporaryArena }) + case .commaSeparated(let list): + return list.contains(where: { $0.requiresTemporaryArena }) } } @@ -453,28 +506,51 @@ extension JavaConversionStep { // NOTE: 'printer' is used if the conversion wants to cause side-effects. // E.g. storing a temporary values into a variable. switch self { - case .pass: + case .placeholder: return placeholder + case .explodedName(let component): + return "\(placeholder)_\(component)" + case .swiftValueSelfSegment: return "\(placeholder).$memorySegment()" - case .call(let function, let withArena): + case .call(let inner, let function, let withArena): + let inner = inner.render(&printer, placeholder) let arenaArg = withArena ? ", arena$" : "" - return "\(function)(\(placeholder)\(arenaArg))" + return "\(function)(\(inner)\(arenaArg))" + + case .method(let inner, let methodName, let arguments, let withArena): + let inner = inner.render(&printer, placeholder) + let args = arguments.map { $0.render(&printer, placeholder) } + let argsStr = (args + (withArena ? ["arena$"] : [])).joined(separator: " ,") + return "\(inner).\(methodName)(\(argsStr))" + + case .constructSwiftValue(let inner, let javaType): + let inner = inner.render(&printer, placeholder) + return "new \(javaType.className!)(\(inner), swiftArena$)" + + case .wrapMemoryAddressUnsafe(let inner, let javaType): + let inner = inner.render(&printer, placeholder) + return "\(javaType.className!).wrapMemoryAddressUnsafe(\(inner), swiftArena$)" + + case .construct(let inner, let javaType): + let inner = inner.render(&printer, placeholder) + return "new \(javaType)(\(inner))" - case .method(let methodName, let arguments, let withArena): - let argsStr = (arguments + (withArena ? ["arena$"] : [])).joined(separator: " ,") - return "\(placeholder).\(methodName)(\(argsStr))" + case .cast(let inner, let javaType): + let inner = inner.render(&printer, placeholder) + return "(\(javaType)) \(inner)" - case .constructSwiftValue(let javaType): - return "new \(javaType.className!)(\(placeholder), swiftArena$)" + case .commaSeparated(let list): + return list.map({ $0.render(&printer, placeholder)}).joined(separator: ", ") - case .construct(let javaType): - return "new \(javaType)(\(placeholder))" + case .constant(let value): + return value - case .cast(let javaType): - return "(\(javaType)) \(placeholder)" + case .readMemorySegment(let inner, let javaType): + let inner = inner.render(&printer, placeholder) + return "\(inner).get(\(ForeignValueLayout(javaType: javaType)!), 0)" } } } diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift index c7e2338de..76284b787 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import JavaTypes +import SwiftJavaConfigurationShared extension FFMSwift2JavaGenerator { func translatedDecl( @@ -24,7 +25,9 @@ extension FFMSwift2JavaGenerator { let translated: TranslatedFunctionDecl? do { - let translation = JavaTranslation(swiftStdlibTypes: self.swiftStdlibTypes) + let translation = JavaTranslation( + config: self.config, + knownTypes: SwiftKnownTypes(symbolTable: lookupContext.symbolTable)) translated = try translation.translate(decl) } catch { self.log.info("Failed to translate: '\(decl.swiftDecl.qualifiedNameForDebug)'; \(error)") @@ -34,452 +37,749 @@ extension FFMSwift2JavaGenerator { translatedDecls[decl] = translated return translated } -} - -/// Represent a parameter in Java code. -struct JavaParameter { - /// The type. - var type: JavaType - /// The name. - var name: String -} + /// Represent a Swift API parameter translated to Java. + struct TranslatedParameter { + /// Java parameter(s) mapped to the Swift parameter. + /// + /// Array because one Swift parameter can be mapped to multiple parameters. + var javaParameters: [JavaParameter] -/// Represent a Swift API parameter translated to Java. -struct TranslatedParameter { - /// Java parameter(s) mapped to the Swift parameter. - /// - /// Array because one Swift parameter can be mapped to multiple parameters. - var javaParameters: [JavaParameter] + /// Describes how to convert the Java parameter to the lowered arguments for + /// the foreign function. + var conversion: JavaConversionStep + } - /// Describes how to convert the Java parameter to the lowered arguments for - /// the foreign function. - var conversion: JavaConversionStep -} + /// Represent a Swift API result translated to Java. + struct TranslatedResult { + /// Java type that represents the Swift result type. + var javaResultType: JavaType + + /// Java annotations that should be propagated from the result type onto the method + var annotations: [JavaAnnotation] = [] + + /// Required indirect return receivers for receiving the result. + /// + /// 'JavaParameter.name' is the suffix for the receiver variable names. For example + /// + /// var _result_pointer = MemorySegment.allocate(...) + /// var _result_count = MemroySegment.allocate(...) + /// downCall(_result_pointer, _result_count) + /// return constructResult(_result_pointer, _result_count) + /// + /// This case, there're two out parameter, named '_pointer' and '_count'. + var outParameters: [JavaParameter] + + /// Describes how to construct the Java result from the foreign function return + /// value and/or the out parameters. + var conversion: JavaConversionStep + } -/// Represent a Swift API result translated to Java. -struct TranslatedResult { - /// Java type that represents the Swift result type. - var javaResultType: JavaType - /// Required indirect return receivers for receiving the result. + /// Translated Java API representing a Swift API. /// - /// 'JavaParameter.name' is the suffix for the receiver variable names. For example - /// - /// var _result_pointer = MemorySegment.allocate(...) - /// var _result_count = MemroySegment.allocate(...) - /// downCall(_result_pointer, _result_count) - /// return constructResult(_result_pointer, _result_count) - /// - /// This case, there're two out parameter, named '_pointer' and '_count'. - var outParameters: [JavaParameter] - - /// Describes how to construct the Java result from the foreign function return - /// value and/or the out parameters. - var conversion: JavaConversionStep -} - + /// Since this holds the lowered signature, and the original `SwiftFunctionSignature` + /// in it, this contains all the API information (except the name) to generate the + /// cdecl thunk, Java binding, and the Java wrapper function. + struct TranslatedFunctionDecl { + /// Java function name. + let name: String -/// Translated Java API representing a Swift API. -/// -/// Since this holds the lowered signature, and the original `SwiftFunctionSignature` -/// in it, this contains all the API information (except the name) to generate the -/// cdecl thunk, Java binding, and the Java wrapper function. -struct TranslatedFunctionDecl { - /// Java function name. - let name: String + /// Functional interfaces required for the Java method. + let functionTypes: [TranslatedFunctionType] - /// Functional interfaces required for the Java method. - let functionTypes: [TranslatedFunctionType] + /// Function signature. + let translatedSignature: TranslatedFunctionSignature - /// Function signature. - let translatedSignature: TranslatedFunctionSignature + /// Cdecl lowered signature. + let loweredSignature: LoweredFunctionSignature - /// Cdecl lowerd signature. - let loweredSignature: LoweredFunctionSignature -} + /// Annotations to include on the Java function declaration + var annotations: [JavaAnnotation] { + self.translatedSignature.annotations + } + } -/// Function signature for a Java API. -struct TranslatedFunctionSignature { - var selfParameter: TranslatedParameter? - var parameters: [TranslatedParameter] - var result: TranslatedResult -} + /// Function signature for a Java API. + struct TranslatedFunctionSignature { + var selfParameter: TranslatedParameter? + var parameters: [TranslatedParameter] + var result: TranslatedResult -/// Represent a Swift closure type in the user facing Java API. -/// -/// Closures are translated to named functional interfaces in Java. -struct TranslatedFunctionType { - var name: String - var parameters: [TranslatedParameter] - var result: TranslatedResult - - /// Whether or not this functional interface with C ABI compatible. - var isCompatibleWithC: Bool { - result.conversion.isPass && parameters.allSatisfy(\.conversion.isPass) + // if the result type implied any annotations, + // propagate them onto the function the result is returned from + var annotations: [JavaAnnotation] { + self.result.annotations + } } -} -extension TranslatedFunctionSignature { - /// Whether or not if the down-calling requires temporary "Arena" which is - /// only used during the down-calling. - var requiresTemporaryArena: Bool { - if self.parameters.contains(where: { $0.conversion.requiresTemporaryArena }) { - return true - } - if self.selfParameter?.conversion.requiresTemporaryArena ?? false { - return true - } - if self.result.conversion.requiresTemporaryArena { - return true + /// Represent a Swift closure type in the user facing Java API. + /// + /// Closures are translated to named functional interfaces in Java. + struct TranslatedFunctionType { + var name: String + var parameters: [TranslatedParameter] + var result: TranslatedResult + var swiftType: SwiftFunctionType + var cdeclType: SwiftFunctionType + + /// Whether or not this functional interface with C ABI compatible. + var isCompatibleWithC: Bool { + result.conversion.isPlaceholder && parameters.allSatisfy(\.conversion.isPlaceholder) } - return false } - /// Whether if the down-calling requires "SwiftArena" or not, which should be - /// passed-in by the API caller. This is needed if the API returns a `SwiftValue` - var requiresSwiftArena: Bool { - return self.result.conversion.requiresSwiftArena - } -} + struct JavaTranslation { + let config: Configuration + var knownTypes: SwiftKnownTypes -struct JavaTranslation { - var swiftStdlibTypes: SwiftStandardLibraryTypes + init(config: Configuration, knownTypes: SwiftKnownTypes) { + self.config = config + self.knownTypes = knownTypes + } - func translate( - _ decl: ImportedFunc - ) throws -> TranslatedFunctionDecl { - let lowering = CdeclLowering(swiftStdlibTypes: self.swiftStdlibTypes) - let loweredSignature = try lowering.lowerFunctionSignature(decl.functionSignature) + func translate(_ decl: ImportedFunc) throws -> TranslatedFunctionDecl { + let lowering = CdeclLowering(knownTypes: knownTypes) + let loweredSignature = try lowering.lowerFunctionSignature(decl.functionSignature) - // Name. - let javaName = switch decl.apiKind { - case .getter: "get\(decl.name.toCamelCase)" - case .setter: "set\(decl.name.toCamelCase)" - case .function, .initializer: decl.name - } + // Name. + let javaName = switch decl.apiKind { + case .getter, .subscriptGetter: decl.javaGetterName + case .setter, .subscriptSetter: decl.javaSetterName + case .function, .initializer, .enumCase: decl.name + } - // Closures. - var funcTypes: [TranslatedFunctionType] = [] - for (idx, param) in decl.functionSignature.parameters.enumerated() { - switch param.type { - case .function(let funcTy): - let paramName = param.parameterName ?? "_\(idx)" - let translatedClosure = try translateFunctionType(name: paramName, swiftType: funcTy) - funcTypes.append(translatedClosure) - case .tuple: - // TODO: Implement - break - default: - break + // Signature. + let translatedSignature = try translate(loweredFunctionSignature: loweredSignature, methodName: javaName) + + // Closures. + var funcTypes: [TranslatedFunctionType] = [] + for (idx, param) in decl.functionSignature.parameters.enumerated() { + switch param.type { + case .function(let funcTy): + let paramName = param.parameterName ?? "_\(idx)" + guard case .function( let cdeclTy) = loweredSignature.parameters[idx].cdeclParameters[0].type else { + preconditionFailure("closure parameter wasn't lowered to a function type; \(funcTy)") + } + let translatedClosure = try translateFunctionType(name: paramName, swiftType: funcTy, cdeclType: cdeclTy) + funcTypes.append(translatedClosure) + case .tuple: + // TODO: Implement + break + default: + break + } } + + return TranslatedFunctionDecl( + name: javaName, + functionTypes: funcTypes, + translatedSignature: translatedSignature, + loweredSignature: loweredSignature + ) } - // Signature. - let translatedSignature = try translate(loweredFunctionSignature: loweredSignature, methodName: javaName) + /// Translate Swift closure type to Java functional interface. + func translateFunctionType( + name: String, + swiftType: SwiftFunctionType, + cdeclType: SwiftFunctionType + ) throws -> TranslatedFunctionType { + var translatedParams: [TranslatedParameter] = [] + + for (i, param) in swiftType.parameters.enumerated() { + let paramName = param.parameterName ?? "_\(i)" + translatedParams.append( + try translateClosureParameter(param.type, convention: param.convention, parameterName: paramName) + ) + } - return TranslatedFunctionDecl( - name: javaName, - functionTypes: funcTypes, - translatedSignature: translatedSignature, - loweredSignature: loweredSignature - ) - } + guard let resultCType = try? CType(cdeclType: swiftType.resultType) else { + throw JavaTranslationError.unhandledType(.function(swiftType)) + } - /// Translate Swift closure type to Java functional interface. - func translateFunctionType( - name: String, - swiftType: SwiftFunctionType - ) throws -> TranslatedFunctionType { - var translatedParams: [TranslatedParameter] = [] - - for (i, param) in swiftType.parameters.enumerated() { - let paramName = param.parameterName ?? "_\(i)" - if let cType = try? CType(cdeclType: param.type) { - let translatedParam = TranslatedParameter( + let transltedResult = TranslatedResult( + javaResultType: resultCType.javaType, + outParameters: [], + conversion: .placeholder + ) + + return TranslatedFunctionType( + name: name, + parameters: translatedParams, + result: transltedResult, + swiftType: swiftType, + cdeclType: cdeclType + ) + } + + func translateClosureParameter( + _ type: SwiftType, + convention: SwiftParameterConvention, + parameterName: String + ) throws -> TranslatedParameter { + if let cType = try? CType(cdeclType: type) { + return TranslatedParameter( javaParameters: [ - JavaParameter(type: cType.javaType, name: paramName) + JavaParameter(name: parameterName, type: cType.javaType) ], - conversion: .pass + conversion: .placeholder ) - translatedParams.append(translatedParam) - continue } - throw JavaTranslationError.unhandledType(.function(swiftType)) - } - guard let resultCType = try? CType(cdeclType: swiftType.resultType) else { - throw JavaTranslationError.unhandledType(.function(swiftType)) - } - - let transltedResult = TranslatedResult( - javaResultType: resultCType.javaType, - outParameters: [], - conversion: .pass - ) - - return TranslatedFunctionType( - name: name, - parameters: translatedParams, - result: transltedResult - ) - } - - /// Translate a Swift API signature to the user-facing Java API signature. - /// - /// Note that the result signature is for the high-level Java API, not the - /// low-level FFM down-calling interface. - func translate( - loweredFunctionSignature: LoweredFunctionSignature, - methodName: String - ) throws -> TranslatedFunctionSignature { - let swiftSignature = loweredFunctionSignature.original - - // 'self' - let selfParameter: TranslatedParameter? - if case .instance(let swiftSelf) = swiftSignature.selfParameter { - selfParameter = try self.translate( - swiftParam: swiftSelf, - loweredParam: loweredFunctionSignature.selfParameter!, - methodName: methodName, - parameterName: swiftSelf.parameterName ?? "self" - ) - } else { - selfParameter = nil + switch type { + case .nominal(let nominal): + if let knownType = nominal.nominalTypeDecl.knownTypeKind { + switch knownType { + case .unsafeRawBufferPointer, .unsafeMutableRawBufferPointer: + return TranslatedParameter( + javaParameters: [ + JavaParameter(name: parameterName, type: .javaForeignMemorySegment) + ], + conversion: .method( + .explodedName(component: "pointer"), + methodName: "reinterpret", + arguments: [ + .explodedName(component: "count") + ], + withArena: false + ) + ) + default: + break + } + } + default: + break + } + throw JavaTranslationError.unhandledType(type) } - // Regular parameters. - let parameters: [TranslatedParameter] = try swiftSignature.parameters.enumerated() - .map { (idx, swiftParam) in - let loweredParam = loweredFunctionSignature.parameters[idx] - let parameterName = swiftParam.parameterName ?? "_\(idx)" - return try self.translate( - swiftParam: swiftParam, - loweredParam: loweredParam, + /// Translate a Swift API signature to the user-facing Java API signature. + /// + /// Note that the result signature is for the high-level Java API, not the + /// low-level FFM down-calling interface. + func translate( + loweredFunctionSignature: LoweredFunctionSignature, + methodName: String + ) throws -> TranslatedFunctionSignature { + let swiftSignature = loweredFunctionSignature.original + + // 'self' + let selfParameter: TranslatedParameter? + if case .instance(let swiftSelf) = swiftSignature.selfParameter { + selfParameter = try self.translateParameter( + type: swiftSelf.type, + convention: swiftSelf.convention, + parameterName: swiftSelf.parameterName ?? "self", + loweredParam: loweredFunctionSignature.selfParameter!, methodName: methodName, - parameterName: parameterName + genericParameters: swiftSignature.genericParameters, + genericRequirements: swiftSignature.genericRequirements ) + } else { + selfParameter = nil } - // Result. - let result = try self.translate( - swiftResult: swiftSignature.result, - loweredResult: loweredFunctionSignature.result - ) - - return TranslatedFunctionSignature( - selfParameter: selfParameter, - parameters: parameters, - result: result - ) - } - - /// Translate a Swift API parameter to the user-facing Java API parameter. - func translate( - swiftParam: SwiftParameter, - loweredParam: LoweredParameter, - methodName: String, - parameterName: String - ) throws -> TranslatedParameter { - let swiftType = swiftParam.type - - // If there is a 1:1 mapping between this Swift type and a C type, that can - // be expressed as a Java primitive type. - if let cType = try? CType(cdeclType: swiftType) { - let javaType = cType.javaType - return TranslatedParameter( - javaParameters: [ - JavaParameter( - type: javaType, - name: parameterName + // Regular parameters. + let parameters: [TranslatedParameter] = try swiftSignature.parameters.enumerated() + .map { (idx, swiftParam) in + let loweredParam = loweredFunctionSignature.parameters[idx] + let parameterName = swiftParam.parameterName ?? "_\(idx)" + return try self.translateParameter( + type: swiftParam.type, + convention: swiftParam.convention, + parameterName: parameterName, + loweredParam: loweredParam, + methodName: methodName, + genericParameters: swiftSignature.genericParameters, + genericRequirements: swiftSignature.genericRequirements ) - ], - conversion: .pass + } + + // Result. + let result = try self.translate( + swiftResult: swiftSignature.result, + loweredResult: loweredFunctionSignature.result ) - } - switch swiftType { - case .metatype: - // Metatype are expressed as 'org.swift.swiftkit.SwiftAnyType' - return TranslatedParameter( - javaParameters: [ - JavaParameter( - type: JavaType.class(package: "org.swift.swiftkit", name: "SwiftAnyType"), - name: parameterName) - ], - conversion: .swiftValueSelfSegment + return TranslatedFunctionSignature( + selfParameter: selfParameter, + parameters: parameters, + result: result ) + } - case .nominal(let swiftNominalType): - if let knownType = swiftNominalType.nominalTypeDecl.knownStandardLibraryType { - if swiftParam.convention == .inout { - // FIXME: Support non-trivial 'inout' for builtin types. - throw JavaTranslationError.inoutNotSupported(swiftType) + /// Translate a Swift API parameter to the user-facing Java API parameter. + func translateParameter( + type swiftType: SwiftType, + convention: SwiftParameterConvention, + parameterName: String, + loweredParam: LoweredParameter, + methodName: String, + genericParameters: [SwiftGenericParameterDeclaration], + genericRequirements: [SwiftGenericRequirement] + ) throws -> TranslatedParameter { + // If the result type should cause any annotations on the method, include them here. + let parameterAnnotations: [JavaAnnotation] = getTypeAnnotations(swiftType: swiftType, config: config) + + // If we need to handle unsigned integers do so here + if config.effectiveUnsignedNumbersMode.needsConversion { + if let unsignedWrapperType = JavaType.unsignedWrapper(for: swiftType) { + return TranslatedParameter( + javaParameters: [ + JavaParameter(name: parameterName, type: unsignedWrapperType, annotations: parameterAnnotations) + ], conversion: .call(.placeholder, function: "UnsignedNumbers.toPrimitive", withArena: false)) } - switch knownType { - case .unsafePointer, .unsafeMutablePointer: - // FIXME: Implement - throw JavaTranslationError.unhandledType(swiftType) - case .unsafeBufferPointer, .unsafeMutableBufferPointer: - // FIXME: Implement + } + + // If there is a 1:1 mapping between this Swift type and a C type, that can + // be expressed as a Java primitive type. + if let cType = try? CType(cdeclType: swiftType) { + let javaType = cType.javaType + return TranslatedParameter( + javaParameters: [ + JavaParameter( + name: parameterName, + type: javaType, + annotations: parameterAnnotations) + ], + conversion: .placeholder + ) + } + + switch swiftType { + case .metatype: + // Metatype are expressed as 'org.swift.swiftkit.SwiftAnyType' + return TranslatedParameter( + javaParameters: [ + JavaParameter( + name: parameterName, + type: JavaType.class(package: "org.swift.swiftkit.ffm", name: "SwiftAnyType"), + annotations: parameterAnnotations + ) + ], + conversion: .swiftValueSelfSegment(.placeholder) + ) + + case .nominal(let swiftNominalType): + if let knownType = swiftNominalType.nominalTypeDecl.knownTypeKind { + if convention == .inout { + // FIXME: Support non-trivial 'inout' for builtin types. + throw JavaTranslationError.inoutNotSupported(swiftType) + } + switch knownType { + case .unsafePointer, .unsafeMutablePointer: + // FIXME: Implement + throw JavaTranslationError.unhandledType(swiftType) + case .unsafeBufferPointer, .unsafeMutableBufferPointer: + // FIXME: Implement + throw JavaTranslationError.unhandledType(swiftType) + + case .unsafeRawBufferPointer, .unsafeMutableRawBufferPointer: + return TranslatedParameter( + javaParameters: [ + JavaParameter(name: parameterName, type: .javaForeignMemorySegment), + ], + conversion: .commaSeparated([ + .placeholder, + .method(.placeholder, methodName: "byteSize", arguments: [], withArena: false) + ]) + ) + + case .optional: + guard let genericArgs = swiftNominalType.genericArguments, genericArgs.count == 1 else { + throw JavaTranslationError.unhandledType(swiftType) + } + return try translateOptionalParameter( + wrappedType: genericArgs[0], + convention: convention, + parameterName: parameterName, + loweredParam: loweredParam, + methodName: methodName, + genericParameters: genericParameters, + genericRequirements: genericRequirements + ) + + case .string: + return TranslatedParameter( + javaParameters: [ + JavaParameter( + name: parameterName, type: .javaLangString + ) + ], + conversion: .call(.placeholder, function: "SwiftRuntime.toCString", withArena: true) + ) + + case .foundationData, .essentialsData: + break + + default: + throw JavaTranslationError.unhandledType(swiftType) + } + } + + // Generic types are not supported yet. + guard swiftNominalType.genericArguments == nil else { throw JavaTranslationError.unhandledType(swiftType) + } - case .string: - return TranslatedParameter( - javaParameters: [ - JavaParameter( - type: .javaLangString, - name: parameterName - ) - ], - conversion: .call(function: "SwiftKit.toCString", withArena: true) + return TranslatedParameter( + javaParameters: [ + JavaParameter( + name: parameterName, type: try translate(swiftType: swiftType) + ) + ], + conversion: .swiftValueSelfSegment(.placeholder) + ) + + case .tuple: + // TODO: Implement. + throw JavaTranslationError.unhandledType(swiftType) + + case .function: + return TranslatedParameter( + javaParameters: [ + JavaParameter( + name: parameterName, type: JavaType.class(package: nil, name: "\(methodName).\(parameterName)")) + ], + conversion: .call(.placeholder, function: "\(methodName).$toUpcallStub", withArena: true) + ) + + case .existential, .opaque, .genericParameter: + if let concreteTy = swiftType.representativeConcreteTypeIn(knownTypes: knownTypes, genericParameters: genericParameters, genericRequirements: genericRequirements) { + return try translateParameter( + type: concreteTy, + convention: convention, + parameterName: parameterName, + loweredParam: loweredParam, + methodName: methodName, + genericParameters: genericParameters, + genericRequirements: genericRequirements ) + } + + // Otherwise, not supported yet. + throw JavaTranslationError.unhandledType(swiftType) + + case .optional(let wrapped): + return try translateOptionalParameter( + wrappedType: wrapped, + convention: convention, + parameterName: parameterName, + loweredParam: loweredParam, + methodName: methodName, + genericParameters: genericParameters, + genericRequirements: genericRequirements + ) + + case .composite: + throw JavaTranslationError.unhandledType(swiftType) + case .array(let elementType): + throw JavaTranslationError.unhandledType(swiftType) + } + } + + /// Translate an Optional Swift API parameter to the user-facing Java API parameter. + func translateOptionalParameter( + wrappedType swiftType: SwiftType, + convention: SwiftParameterConvention, + parameterName: String, + loweredParam: LoweredParameter, + methodName: String, + genericParameters: [SwiftGenericParameterDeclaration], + genericRequirements: [SwiftGenericRequirement] + ) throws -> TranslatedParameter { + // If there is a 1:1 mapping between this Swift type and a C type, that can + // be expressed as a Java primitive type. + if let cType = try? CType(cdeclType: swiftType) { + let (translatedClass, lowerFunc) = switch cType.javaType { + case .int: ("OptionalInt", "toOptionalSegmentInt") + case .long: ("OptionalLong", "toOptionalSegmentLong") + case .double: ("OptionalDouble", "toOptionalSegmentDouble") + case .boolean: ("Optional", "toOptionalSegmentBoolean") + case .byte: ("Optional", "toOptionalSegmentByte") + case .char: ("Optional", "toOptionalSegmentCharacter") + case .short: ("Optional", "toOptionalSegmentShort") + case .float: ("Optional", "toOptionalSegmentFloat") default: - throw JavaTranslationError.unhandledType(swiftType) + throw JavaTranslationError.unhandledType(.optional(swiftType)) } + return TranslatedParameter( + javaParameters: [ + JavaParameter(name: parameterName, type: JavaType(className: translatedClass)) + ], + conversion: .call(.placeholder, function: "SwiftRuntime.\(lowerFunc)", withArena: true) + ) } - // Generic types are not supported yet. - guard swiftNominalType.genericArguments == nil else { - throw JavaTranslationError.unhandledType(swiftType) - } + switch swiftType { + case .nominal(let nominal): + if let knownType = nominal.nominalTypeDecl.knownTypeKind { + switch knownType { + case .foundationData, .foundationDataProtocol, .essentialsData, .essentialsDataProtocol: + break + default: + throw JavaTranslationError.unhandledType(.optional(swiftType)) + } + } - return TranslatedParameter( - javaParameters: [ - JavaParameter( - type: try translate(swiftType: swiftType), - name: parameterName + let translatedTy = try self.translate(swiftType: swiftType) + return TranslatedParameter( + javaParameters: [ + JavaParameter(name: parameterName, type: JavaType(className: "Optional<\(translatedTy.description)>")) + ], + conversion: .call(.placeholder, function: "SwiftRuntime.toOptionalSegmentInstance", withArena: false) + ) + case .existential, .opaque, .genericParameter: + if let concreteTy = swiftType.representativeConcreteTypeIn(knownTypes: knownTypes, genericParameters: genericParameters, genericRequirements: genericRequirements) { + return try translateOptionalParameter( + wrappedType: concreteTy, + convention: convention, + parameterName: parameterName, + loweredParam: loweredParam, + methodName: methodName, + genericParameters: genericParameters, + genericRequirements: genericRequirements ) - ], - conversion: .swiftValueSelfSegment - ) + } + throw JavaTranslationError.unhandledType(.optional(swiftType)) + case .tuple(let tuple): + if tuple.count == 1 { + return try translateOptionalParameter( + wrappedType: tuple[0], + convention: convention, + parameterName: parameterName, + loweredParam: loweredParam, + methodName: methodName, + genericParameters: genericParameters, + genericRequirements: genericRequirements + ) + } + throw JavaTranslationError.unhandledType(.optional(swiftType)) + default: + throw JavaTranslationError.unhandledType(.optional(swiftType)) + } + } - case .tuple: - // TODO: Implement. - throw JavaTranslationError.unhandledType(swiftType) - - case .function: - return TranslatedParameter( - javaParameters: [ - JavaParameter( - type: JavaType.class(package: nil, name: "\(methodName).\(parameterName)"), - name: parameterName) - ], - conversion: .call(function: "\(methodName).$toUpcallStub", withArena: true) - ) + func unsignedResultConversion(_ from: SwiftType, to javaType: JavaType, + mode: JExtractUnsignedIntegerMode) -> JavaConversionStep { + switch mode { + case .annotate: + return .placeholder // no conversions - case .optional: - throw JavaTranslationError.unhandledType(swiftType) - } - } + case .wrapGuava: + guard let typeName = javaType.fullyQualifiedClassName else { + fatalError("Missing target class name for result conversion step from \(from) to \(javaType)") + } - /// Translate a Swift API result to the user-facing Java API result. - func translate( - swiftResult: SwiftResult, - loweredResult: LoweredResult - ) throws -> TranslatedResult { - let swiftType = swiftResult.type - - // If there is a 1:1 mapping between this Swift type and a C type, that can - // be expressed as a Java primitive type. - if let cType = try? CType(cdeclType: swiftType) { - let javaType = cType.javaType - return TranslatedResult( - javaResultType: javaType, - outParameters: [], - conversion: .pass - ) + switch from { + case .nominal(let nominal): + switch nominal.nominalTypeDecl.knownTypeKind { + case .uint8: + return .call(.placeholder, function: "\(typeName).fromIntBits", withArena: false) + case .uint16: + return .placeholder // no conversion, UInt16 can be returned as-is and will be seen as char by Java + case .uint32: + return .call(.placeholder, function: "\(typeName).fromIntBits", withArena: false) + case .uint64: + return .call(.placeholder, function: "\(typeName).fromLongBits", withArena: false) + default: + fatalError("unsignedResultConversion: Unsupported conversion from \(from) to \(javaType)") + } + default: + fatalError("unsignedResultConversion: Unsupported conversion from \(from) to \(javaType)") + } + } } - switch swiftType { - case .metatype(_): - // Metatype are expressed as 'org.swift.swiftkit.SwiftAnyType' - let javaType = JavaType.class(package: "org.swift.swiftkit", name: "SwiftAnyType") - return TranslatedResult( - javaResultType: javaType, - outParameters: [], - conversion: .construct(javaType) - ) + /// Translate a Swift API result to the user-facing Java API result. + func translate( + swiftResult: SwiftResult, + loweredResult: LoweredResult + ) throws -> TranslatedResult { + let swiftType = swiftResult.type + + // If we need to handle unsigned integers do so here + if config.effectiveUnsignedNumbersMode.needsConversion { + if let unsignedWrapperType = JavaType.unsignedWrapper(for: swiftType) /* and we're in safe wrapper mode */ { + return TranslatedResult( + javaResultType: unsignedWrapperType, + outParameters: [], + conversion: unsignedResultConversion( + swiftType, to: unsignedWrapperType, + mode: self.config.effectiveUnsignedNumbersMode) + ) + } + } - case .nominal(let swiftNominalType): - if let knownType = swiftNominalType.nominalTypeDecl.knownStandardLibraryType { - switch knownType { - case .unsafePointer, .unsafeMutablePointer: - // FIXME: Implement - throw JavaTranslationError.unhandledType(swiftType) - case .unsafeBufferPointer, .unsafeMutableBufferPointer: - // FIXME: Implement - throw JavaTranslationError.unhandledType(swiftType) - case .string: - // FIXME: Implement - throw JavaTranslationError.unhandledType(swiftType) - default: + // If the result type should cause any annotations on the method, include them here. + let resultAnnotations: [JavaAnnotation] = getTypeAnnotations(swiftType: swiftType, config: config) + + // If there is a 1:1 mapping between this Swift type and a C type, that can + // be expressed as a Java primitive type. + if let cType = try? CType(cdeclType: swiftType) { + let javaType = cType.javaType + return TranslatedResult( + javaResultType: javaType, + annotations: resultAnnotations, + outParameters: [], + conversion: .placeholder + ) + } + + switch swiftType { + case .metatype(_): + // Metatype are expressed as 'org.swift.swiftkit.SwiftAnyType' + let javaType = JavaType.class(package: "org.swift.swiftkit.ffm", name: "SwiftAnyType") + return TranslatedResult( + javaResultType: javaType, + annotations: resultAnnotations, + outParameters: [], + conversion: .construct(.placeholder, javaType) + ) + + case .nominal(let swiftNominalType): + if let knownType = swiftNominalType.nominalTypeDecl.knownTypeKind { + switch knownType { + case .unsafeRawBufferPointer, .unsafeMutableRawBufferPointer: + return TranslatedResult( + javaResultType: .javaForeignMemorySegment, + annotations: resultAnnotations, + outParameters: [ + JavaParameter(name: "pointer", type: .javaForeignMemorySegment), + JavaParameter(name: "count", type: .long), + ], + conversion: .method( + .readMemorySegment(.explodedName(component: "pointer"), as: .javaForeignMemorySegment), + methodName: "reinterpret", + arguments: [ + .readMemorySegment(.explodedName(component: "count"), as: .long), + ], + withArena: false + ) + ) + + case .foundationData, .essentialsData: + break + + case .unsafePointer, .unsafeMutablePointer: + // FIXME: Implement + throw JavaTranslationError.unhandledType(swiftType) + case .unsafeBufferPointer, .unsafeMutableBufferPointer: + // FIXME: Implement + throw JavaTranslationError.unhandledType(swiftType) + case .string: + // FIXME: Implement + throw JavaTranslationError.unhandledType(swiftType) + default: + throw JavaTranslationError.unhandledType(swiftType) + } + } + + // Generic types are not supported yet. + guard swiftNominalType.genericArguments == nil else { throw JavaTranslationError.unhandledType(swiftType) } + + let javaType: JavaType = .class(package: nil, name: swiftNominalType.nominalTypeDecl.name) + return TranslatedResult( + javaResultType: javaType, + annotations: resultAnnotations, + outParameters: [ + JavaParameter(name: "", type: javaType) + ], + conversion: .wrapMemoryAddressUnsafe(.placeholder, javaType) + ) + + case .tuple: + // TODO: Implement. + throw JavaTranslationError.unhandledType(swiftType) + + case .genericParameter, .optional, .function, .existential, .opaque, .composite, .array: + throw JavaTranslationError.unhandledType(swiftType) } - // Generic types are not supported yet. - guard swiftNominalType.genericArguments == nil else { + } + + func translate( + swiftType: SwiftType + ) throws -> JavaType { + guard let nominalName = swiftType.asNominalTypeDeclaration?.name else { throw JavaTranslationError.unhandledType(swiftType) } + return .class(package: nil, name: nominalName) + } + } - let javaType: JavaType = .class(package: nil, name: swiftNominalType.nominalTypeDecl.name) - return TranslatedResult( - javaResultType: javaType, - outParameters: [ - JavaParameter(type: javaType, name: "") - ], - conversion: .constructSwiftValue(javaType) - ) + /// Describes how to convert values between Java types and FFM types. + enum JavaConversionStep { + // The input + case placeholder - case .tuple: - // TODO: Implement. - throw JavaTranslationError.unhandledType(swiftType) + // The input exploded into components. + case explodedName(component: String) - case .optional, .function: - throw JavaTranslationError.unhandledType(swiftType) - } + // A fixed value + case constant(String) - } + // 'value.$memorySegment()' + indirect case swiftValueSelfSegment(JavaConversionStep) - func translate( - swiftType: SwiftType - ) throws -> JavaType { - guard let nominalName = swiftType.asNominalTypeDeclaration?.name else { - throw JavaTranslationError.unhandledType(swiftType) - } - return .class(package: nil, name: nominalName) - } -} + // call specified function using the placeholder as arguments. + // If `withArena` is true, `arena$` argument is added. + indirect case call(JavaConversionStep, function: String, withArena: Bool) -/// Describes how to convert values between Java types and FFM types. -enum JavaConversionStep { - // Pass through. - case pass + // Apply a method on the placeholder. + // If `withArena` is true, `arena$` argument is added. + indirect case method(JavaConversionStep, methodName: String, arguments: [JavaConversionStep] = [], withArena: Bool) - // 'value.$memorySegment()' - case swiftValueSelfSegment + // Call 'new \(Type)(\(placeholder), swiftArena$)'. + indirect case constructSwiftValue(JavaConversionStep, JavaType) - // call specified function using the placeholder as arguments. - // If `withArena` is true, `arena$` argument is added. - case call(function: String, withArena: Bool) + /// Call the `MyType.wrapMemoryAddressUnsafe` in order to wrap a memory address using the Java binding type + indirect case wrapMemoryAddressUnsafe(JavaConversionStep, JavaType) - // Apply a method on the placeholder. - // If `withArena` is true, `arena$` argument is added. - case method(methodName: String, arguments: [String] = [], withArena: Bool) + // Construct the type using the placeholder as arguments. + indirect case construct(JavaConversionStep, JavaType) - // Call 'new \(Type)(\(placeholder), swiftArena$)'. - case constructSwiftValue(JavaType) + // Casting the placeholder to the certain type. + indirect case cast(JavaConversionStep, JavaType) - // Construct the type using the placeholder as arguments. - case construct(JavaType) + // Convert the results of the inner steps to a comma separated list. + indirect case commaSeparated([JavaConversionStep]) - // Casting the placeholder to the certain type. - case cast(JavaType) + // Refer an exploded argument suffixed with `_\(name)`. + indirect case readMemorySegment(JavaConversionStep, as: JavaType) - var isPass: Bool { - return if case .pass = self { true } else { false } + var isPlaceholder: Bool { + return if case .placeholder = self { true } else { false } + } + } +} + + +extension FFMSwift2JavaGenerator.TranslatedFunctionSignature { + /// Whether or not if the down-calling requires temporary "Arena" which is + /// only used during the down-calling. + var requiresTemporaryArena: Bool { + if self.parameters.contains(where: { $0.conversion.requiresTemporaryArena }) { + return true + } + if self.selfParameter?.conversion.requiresTemporaryArena ?? false { + return true + } + if self.result.conversion.requiresTemporaryArena { + return true + } + return false + } + + /// Whether if the down-calling requires "SwiftArena" or not, which should be + /// passed-in by the API caller. This is needed if the API returns a `SwiftValue` + var requiresSwiftArena: Bool { + return self.result.conversion.requiresSwiftArena } } @@ -495,7 +795,7 @@ extension CType { case .integral(.signed(bits: 32)): return .int case .integral(.signed(bits: 64)): return .long case .integral(.unsigned(bits: 8)): return .byte - case .integral(.unsigned(bits: 16)): return .short + case .integral(.unsigned(bits: 16)): return .char // char is Java's only unsigned primitive, we can use it! case .integral(.unsigned(bits: 32)): return .int case .integral(.unsigned(bits: 64)): return .long @@ -532,10 +832,10 @@ extension CType { case .integral(.signed(bits: 32)): return .SwiftInt32 case .integral(.signed(bits: 64)): return .SwiftInt64 - case .integral(.unsigned(bits: 8)): return .SwiftInt8 - case .integral(.unsigned(bits: 16)): return .SwiftInt16 - case .integral(.unsigned(bits: 32)): return .SwiftInt32 - case .integral(.unsigned(bits: 64)): return .SwiftInt64 + case .integral(.unsigned(bits: 8)): return .SwiftUInt8 + case .integral(.unsigned(bits: 16)): return .SwiftUInt16 + case .integral(.unsigned(bits: 32)): return .SwiftUInt32 + case .integral(.unsigned(bits: 64)): return .SwiftUInt64 case .floating(.double): return .SwiftDouble case .floating(.float): return .SwiftFloat @@ -558,6 +858,6 @@ extension CType { } enum JavaTranslationError: Error { - case inoutNotSupported(SwiftType) + case inoutNotSupported(SwiftType, file: String = #file, line: Int = #line) case unhandledType(SwiftType, file: String = #file, line: Int = #line) } diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift index 02d715a00..f233ef007 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift @@ -21,43 +21,82 @@ extension FFMSwift2JavaGenerator { try writeSwiftThunkSources(printer: &printer) } + package func writeSwiftExpectedEmptySources() throws { + let pendingFileCount = self.expectedOutputSwiftFiles.count + guard pendingFileCount > 0 else { + return // no need to write any empty files, yay + } + + print("[swift-java] Write empty [\(self.expectedOutputSwiftFiles.count)] 'expected' files in: \(swiftOutputDirectory)/") + + for expectedFileName in self.expectedOutputSwiftFiles { + log.debug("Write SwiftPM-'expected' empty file: \(expectedFileName.bold)") + + + var printer = CodePrinter() + printer.print("// Empty file generated on purpose") + _ = try printer.writeContents( + outputDirectory: self.swiftOutputDirectory, + javaPackagePath: nil, + filename: expectedFileName) + } + } + package func writeSwiftThunkSources(printer: inout CodePrinter) throws { let moduleFilenameBase = "\(self.swiftModuleName)Module+SwiftJava" let moduleFilename = "\(moduleFilenameBase).swift" do { - log.info("Printing contents: \(moduleFilename)") + log.debug("Printing contents: \(moduleFilename)") try printGlobalSwiftThunkSources(&printer) if let outputFile = try printer.writeContents( outputDirectory: self.swiftOutputDirectory, javaPackagePath: nil, - filename: moduleFilename) - { - print("[swift-java] Generated: \(moduleFilenameBase.bold).swift (at \(outputFile)") + filename: moduleFilename) { + log.info("Generated: \(moduleFilenameBase.bold).swift (at \(outputFile.absoluteString))") + self.expectedOutputSwiftFiles.remove(moduleFilename) } } catch { log.warning("Failed to write to Swift thunks: \(moduleFilename)") } // === All types - for (_, ty) in self.analysis.importedTypes.sorted(by: { (lhs, rhs) in lhs.key < rhs.key }) { - let fileNameBase = "\(ty.swiftNominal.qualifiedName)+SwiftJava" - let filename = "\(fileNameBase).swift" - log.info("Printing contents: \(filename)") + // We have to write all types to their corresponding output file that matches the file they were declared in, + // because otherwise SwiftPM plugins will not pick up files apropriately -- we expect 1 output +SwiftJava.swift file for every input. + for group: (key: String, value: [Dictionary.Element]) in Dictionary(grouping: self.analysis.importedTypes, by: { $0.value.sourceFilePath }) { + log.warning("Writing types in file group: \(group.key): \(group.value.map(\.key))") + + let importedTypesForThisFile = group.value + .map(\.value) + .sorted(by: { $0.qualifiedName < $1.qualifiedName }) + + let inputFileName = "\(group.key)".split(separator: "/").last ?? "__Unknown.swift" + let filename = "\(inputFileName)".replacing(".swift", with: "+SwiftJava.swift") + + for ty in importedTypesForThisFile { + log.info("Printing Swift thunks for type: \(ty.qualifiedName.bold)") + printer.printSeparator("Thunks for \(ty.qualifiedName)") + + do { + try printSwiftThunkSources(&printer, ty: ty) + } catch { + log.warning("Failed to print to Swift thunks for type'\(ty.qualifiedName)' to '\(filename)', error: \(error)") + } + + } + log.warning("Write Swift thunks file: \(filename.bold)") do { - try printSwiftThunkSources(&printer, ty: ty) - if let outputFile = try printer.writeContents( outputDirectory: self.swiftOutputDirectory, javaPackagePath: nil, - filename: filename) - { - print("[swift-java] Generated: \(fileNameBase.bold).swift (at \(outputFile)") + filename: filename) { + log.info("Done writing Swift thunks to: \(outputFile.absoluteString)") + self.expectedOutputSwiftFiles.remove(filename) } } catch { - log.warning("Failed to write to Swift thunks: \(filename)") + log.warning("Failed to write to Swift thunks: \(filename), error: \(error)") } } } @@ -69,10 +108,12 @@ extension FFMSwift2JavaGenerator { """ // Generated by swift-java - import SwiftKitSwift + import SwiftRuntimeFunctions """) + printSwiftThunkImports(&printer) + for thunk in stt.renderGlobalThunks() { printer.print(thunk) printer.println() @@ -95,16 +136,72 @@ extension FFMSwift2JavaGenerator { """ // Generated by swift-java - import SwiftKitSwift + import SwiftRuntimeFunctions """ ) + printSwiftThunkImports(&printer) + for thunk in stt.renderThunks(forType: ty) { printer.print("\(thunk)") printer.print("") } } + + func printSwiftThunkImports(_ printer: inout CodePrinter) { + let mainSymbolSourceModules = Set( + self.lookupContext.symbolTable.importedModules.values.filter { $0.alternativeModules?.isMainSourceOfSymbols ?? false }.map(\.moduleName) + ) + + for module in self.lookupContext.symbolTable.importedModules.keys.sorted() { + guard module != "Swift" else { + continue + } + + guard let alternativeModules = self.lookupContext.symbolTable.importedModules[module]?.alternativeModules else { + printer.print("import \(module)") + continue + } + + // Try to print only on main module from relation chain as it has every other module. + guard !mainSymbolSourceModules.isDisjoint(with: alternativeModules.moduleNames) || alternativeModules.isMainSourceOfSymbols else { + if !alternativeModules.isMainSourceOfSymbols { + printer.print("import \(module)") + } + continue + } + + var importGroups: [String: [String]] = [:] + for name in alternativeModules.moduleNames { + guard let otherModule = self.lookupContext.symbolTable.importedModules[name] else { continue } + + let groupKey = otherModule.requiredAvailablityOfModuleWithName ?? otherModule.moduleName + importGroups[groupKey, default: []].append(otherModule.moduleName) + } + + for (index, group) in importGroups.keys.sorted().enumerated() { + if index > 0 && importGroups.keys.count > 1 { + printer.print("#elseif canImport(\(group))") + } else { + printer.print("#if canImport(\(group))") + } + + for groupModule in importGroups[group] ?? [] { + printer.print("import \(groupModule)") + } + } + + if (importGroups.keys.isEmpty) { + printer.print("import \(module)") + } else { + printer.print("#else") + printer.print("import \(module)") + printer.print("#endif") + } + } + printer.println() + } } struct SwiftThunkTranslator { @@ -182,8 +279,7 @@ struct SwiftThunkTranslator { let thunkFunc = translated.loweredSignature.cdeclThunk( cName: thunkName, swiftAPIName: decl.name, - as: decl.apiKind, - stdlibTypes: st.swiftStdlibTypes + as: decl.apiKind ) return [DeclSyntax(thunkFunc)] } diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift index 7cd0075b6..c445d5c99 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift @@ -15,16 +15,18 @@ import JavaTypes import SwiftSyntax import SwiftSyntaxBuilder +import SwiftJavaConfigurationShared +import struct Foundation.URL package class FFMSwift2JavaGenerator: Swift2JavaGenerator { let log: Logger + let config: Configuration let analysis: AnalysisResult let swiftModuleName: String let javaPackage: String let swiftOutputDirectory: String let javaOutputDirectory: String - let swiftStdlibTypes: SwiftStandardLibraryTypes - let symbolTable: SwiftSymbolTable + let lookupContext: SwiftTypeLookupContext var javaPackagePath: String { javaPackage.replacingOccurrences(of: ".", with: "/") @@ -35,29 +37,50 @@ package class FFMSwift2JavaGenerator: Swift2JavaGenerator { /// Cached Java translation result. 'nil' indicates failed translation. var translatedDecls: [ImportedFunc: TranslatedFunctionDecl?] = [:] + /// Because we need to write empty files for SwiftPM, keep track which files we didn't write yet, + /// and write an empty file for those. + var expectedOutputSwiftFiles: Set package init( + config: Configuration, translator: Swift2JavaTranslator, javaPackage: String, swiftOutputDirectory: String, javaOutputDirectory: String ) { self.log = Logger(label: "ffm-generator", logLevel: translator.log.logLevel) + self.config = config self.analysis = translator.result self.swiftModuleName = translator.swiftModuleName self.javaPackage = javaPackage self.swiftOutputDirectory = swiftOutputDirectory self.javaOutputDirectory = javaOutputDirectory - self.symbolTable = translator.symbolTable - self.swiftStdlibTypes = translator.swiftStdlibTypes + self.lookupContext = translator.lookupContext + + // If we are forced to write empty files, construct the expected outputs + if translator.config.writeEmptyFiles ?? false { + self.expectedOutputSwiftFiles = Set(translator.inputs.compactMap { (input) -> String? in + guard let filePathPart = input.path.split(separator: "/\(translator.swiftModuleName)/").last else { + return nil + } + + return String(filePathPart.replacing(".swift", with: "+SwiftJava.swift")) + }) + self.expectedOutputSwiftFiles.insert("\(translator.swiftModuleName)Module+SwiftJava.swift") + self.expectedOutputSwiftFiles.insert("Foundation+SwiftJava.swift") + } else { + self.expectedOutputSwiftFiles = [] + } } func generate() throws { try writeSwiftThunkSources() - print("[swift-java] Generated Swift sources (module: '\(self.swiftModuleName)') in: \(swiftOutputDirectory)/") + log.info("Generated Swift sources (module: '\(self.swiftModuleName)') in: \(swiftOutputDirectory)/") try writeExportedJavaSources() - print("[swift-java] Generated Java sources (package: '\(javaPackage)') in: \(javaOutputDirectory)/") + log.info("Generated Java sources (package: '\(javaPackage)') in: \(javaOutputDirectory)/") + + try writeSwiftExpectedEmptySources() } } @@ -68,16 +91,17 @@ extension FFMSwift2JavaGenerator { /// Default set Java imports for every generated file static let defaultJavaImports: Array = [ - "org.swift.swiftkit.*", - "org.swift.swiftkit.SwiftKit", - "org.swift.swiftkit.util.*", + "org.swift.swiftkit.core.*", + "org.swift.swiftkit.core.util.*", + "org.swift.swiftkit.ffm.*", + + // NonNull, Unsigned and friends + "org.swift.swiftkit.core.annotations.*", // Necessary for native calls and type mapping "java.lang.foreign.*", "java.lang.invoke.*", - "java.util.Arrays", - "java.util.stream.Collectors", - "java.util.concurrent.atomic.*", + "java.util.*", "java.nio.charset.StandardCharsets", ] } @@ -96,7 +120,7 @@ extension FFMSwift2JavaGenerator { package func writeExportedJavaSources(printer: inout CodePrinter) throws { for (_, ty) in analysis.importedTypes.sorted(by: { (lhs, rhs) in lhs.key < rhs.key }) { let filename = "\(ty.swiftNominal.name).java" - log.info("Printing contents: \(filename)") + log.debug("Printing contents: \(filename)") printImportedNominal(&printer, ty) if let outputFile = try printer.writeContents( @@ -104,13 +128,13 @@ extension FFMSwift2JavaGenerator { javaPackagePath: javaPackagePath, filename: filename ) { - print("[swift-java] Generated: \(ty.swiftNominal.name.bold).java (at \(outputFile))") + log.info("Generated: \((ty.swiftNominal.name.bold + ".java").bold) (at \(outputFile.absoluteString))") } } do { let filename = "\(self.swiftModuleName).java" - log.info("Printing contents: \(filename)") + log.debug("Printing contents: \(filename)") printModule(&printer) if let outputFile = try printer.writeContents( @@ -118,7 +142,7 @@ extension FFMSwift2JavaGenerator { javaPackagePath: javaPackagePath, filename: filename) { - print("[swift-java] Generated: \(self.swiftModuleName).java (at \(outputFile))") + log.info("Generated: \((self.swiftModuleName + ".java").bold) (at \(outputFile.absoluteString))") } } } @@ -138,7 +162,12 @@ extension FFMSwift2JavaGenerator { printImports(&printer) printModuleClass(&printer) { printer in - // TODO: print all "static" methods + + for decl in analysis.importedGlobalVariables { + self.log.trace("Print imported decl: \(decl)") + printFunctionDowncallMethods(&printer, decl) + } + for decl in analysis.importedGlobalFuncs { self.log.trace("Print imported decl: \(decl)") printFunctionDowncallMethods(&printer, decl) @@ -149,7 +178,7 @@ extension FFMSwift2JavaGenerator { func printImportedNominal(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { printHeader(&printer) printPackage(&printer) - printImports(&printer) + printImports(&printer) // TODO: we could have some imports be driven from types used in the generated decl printNominal(&printer, decl) { printer in // We use a static field to abuse the initialization order such that by the time we get type metadata, @@ -159,8 +188,8 @@ extension FFMSwift2JavaGenerator { @SuppressWarnings("unused") private static final boolean INITIALIZED_LIBS = initializeLibs(); static boolean initializeLibs() { - System.loadLibrary(SwiftKit.STDLIB_DYLIB_NAME); - System.loadLibrary("SwiftKitSwift"); + System.loadLibrary(SwiftLibraries.LIB_NAME_SWIFT_CORE); + System.loadLibrary(SwiftLibraries.LIB_NAME_SWIFT_RUNTIME_FUNCTIONS); System.loadLibrary(LIB_NAME); return true; } @@ -181,9 +210,22 @@ extension FFMSwift2JavaGenerator { printer.print( """ - public \(decl.swiftNominal.name)(MemorySegment segment, SwiftArena arena) { + private \(decl.swiftNominal.name)(MemorySegment segment, AllocatingSwiftArena arena) { super(segment, arena); } + + /** + * Assume that the passed {@code MemorySegment} represents a memory address of a {@link \(decl.swiftNominal.name)}. + *

+ * Warnings: + *

    + *
  • No checks are performed about the compatibility of the pointed at memory and the actual \(decl.swiftNominal.name) types.
  • + *
  • This operation does not copy, or retain, the pointed at pointer, so its lifetime must be ensured manually to be valid when wrapping.
  • + *
+ */ + public static \(decl.swiftNominal.name) wrapMemoryAddressUnsafe(MemorySegment selfPointer, AllocatingSwiftArena swiftArena) { + return new \(decl.swiftNominal.name)(selfPointer, swiftArena); + } """ ) @@ -243,7 +285,10 @@ extension FFMSwift2JavaGenerator { parentProtocol = "SwiftValue" } - printer.printBraceBlock("public final class \(decl.swiftNominal.name) extends SwiftInstance implements \(parentProtocol)") { + if decl.swiftNominal.isSendable { + printer.print("@ThreadSafe // Sendable") + } + printer.printBraceBlock("public final class \(decl.swiftNominal.name) extends FFMSwiftInstance implements \(parentProtocol)") { printer in // Constants printClassConstants(printer: &printer) @@ -292,10 +337,11 @@ extension FFMSwift2JavaGenerator { """ static final SymbolLookup SYMBOL_LOOKUP = getSymbolLookup(); private static SymbolLookup getSymbolLookup() { - // Ensure Swift and our Lib are loaded during static initialization of the class. - SwiftKit.loadLibrary("swiftCore"); - SwiftKit.loadLibrary("SwiftKitSwift"); - SwiftKit.loadLibrary(LIB_NAME); + if (SwiftLibraries.AUTO_LOAD_LIBS) { + System.loadLibrary(SwiftLibraries.LIB_NAME_SWIFT_CORE); + System.loadLibrary(SwiftLibraries.LIB_NAME_SWIFT_RUNTIME_FUNCTIONS); + System.loadLibrary(LIB_NAME); + } if (PlatformUtils.isMacOS()) { return SymbolLookup.libraryLookup(System.mapLibraryName(LIB_NAME), LIBRARY_ARENA) @@ -357,7 +403,7 @@ extension FFMSwift2JavaGenerator { public String toString() { return getClass().getSimpleName() + "(" - + SwiftKit.nameOfSwiftType($swiftType().$memorySegment(), true) + + SwiftRuntime.nameOfSwiftType($swiftType().$memorySegment(), true) + ")@" + $memorySegment(); } diff --git a/Sources/JExtractSwiftLib/FFM/ForeignValueLayouts.swift b/Sources/JExtractSwiftLib/FFM/ForeignValueLayouts.swift index 7cf1d4df0..329efaad3 100644 --- a/Sources/JExtractSwiftLib/FFM/ForeignValueLayouts.swift +++ b/Sources/JExtractSwiftLib/FFM/ForeignValueLayouts.swift @@ -43,6 +43,7 @@ public struct ForeignValueLayout: CustomStringConvertible, Equatable { case .long: self = .SwiftInt64 case .float: self = .SwiftFloat case .double: self = .SwiftDouble + case .javaForeignMemorySegment: self = .SwiftPointer case .array, .class, .void: return nil } } @@ -66,11 +67,19 @@ extension ForeignValueLayout { public static let SwiftBool = Self(javaConstant: "SWIFT_BOOL") public static let SwiftInt = Self(javaConstant: "SWIFT_INT") + public static let SwiftUInt = Self(javaConstant: "SWIFT_UINT") + public static let SwiftInt64 = Self(javaConstant: "SWIFT_INT64") + public static let SwiftUInt64 = Self(javaConstant: "SWIFT_UINT64") + public static let SwiftInt32 = Self(javaConstant: "SWIFT_INT32") + public static let SwiftUInt32 = Self(javaConstant: "SWIFT_UINT32") + public static let SwiftInt16 = Self(javaConstant: "SWIFT_INT16") public static let SwiftUInt16 = Self(javaConstant: "SWIFT_UINT16") + public static let SwiftInt8 = Self(javaConstant: "SWIFT_INT8") + public static let SwiftUInt8 = Self(javaConstant: "SWIFT_UINT8") public static let SwiftFloat = Self(javaConstant: "SWIFT_FLOAT") public static let SwiftDouble = Self(javaConstant: "SWIFT_DOUBLE") diff --git a/Sources/JExtractSwiftLib/ImportedDecls.swift b/Sources/JExtractSwiftLib/ImportedDecls.swift index e98d29b30..aa4014d21 100644 --- a/Sources/JExtractSwiftLib/ImportedDecls.swift +++ b/Sources/JExtractSwiftLib/ImportedDecls.swift @@ -22,23 +22,93 @@ package enum SwiftAPIKind { case initializer case getter case setter + case enumCase + case subscriptGetter + case subscriptSetter } /// Describes a Swift nominal type (e.g., a class, struct, enum) that has been /// imported and is being translated into Java. -package class ImportedNominalType: ImportedDecl { +package final class ImportedNominalType: ImportedDecl { let swiftNominal: SwiftNominalTypeDeclaration + // The short path from module root to the file in which this nominal was originally declared. + // E.g. for `Sources/Example/My/Types.swift` it would be `My/Types.swift`. + package var sourceFilePath: String { + self.swiftNominal.sourceFilePath + } + package var initializers: [ImportedFunc] = [] package var methods: [ImportedFunc] = [] package var variables: [ImportedFunc] = [] + package var cases: [ImportedEnumCase] = [] + var inheritedTypes: [SwiftType] + package var parent: SwiftNominalTypeDeclaration? - init(swiftNominal: SwiftNominalTypeDeclaration) { + init(swiftNominal: SwiftNominalTypeDeclaration, lookupContext: SwiftTypeLookupContext) throws { self.swiftNominal = swiftNominal + self.inheritedTypes = swiftNominal.inheritanceTypes?.compactMap { + try? SwiftType($0.type, lookupContext: lookupContext) + } ?? [] + self.parent = swiftNominal.parent + } + + var swiftType: SwiftType { + return .nominal(.init(nominalTypeDecl: swiftNominal)) + } + + var qualifiedName: String { + self.swiftNominal.qualifiedName + } +} + +public final class ImportedEnumCase: ImportedDecl, CustomStringConvertible { + /// The case name + public var name: String + + /// The enum parameters + var parameters: [SwiftEnumCaseParameter] + + var swiftDecl: any DeclSyntaxProtocol + + var enumType: SwiftNominalType + + /// A function that represents the Swift static "initializer" for cases + var caseFunction: ImportedFunc + + init( + name: String, + parameters: [SwiftEnumCaseParameter], + swiftDecl: any DeclSyntaxProtocol, + enumType: SwiftNominalType, + caseFunction: ImportedFunc + ) { + self.name = name + self.parameters = parameters + self.swiftDecl = swiftDecl + self.enumType = enumType + self.caseFunction = caseFunction + } + + public var description: String { + """ + ImportedEnumCase { + name: \(name), + parameters: \(parameters), + swiftDecl: \(swiftDecl), + enumType: \(enumType), + caseFunction: \(caseFunction) + } + """ } +} - var javaClassName: String { - swiftNominal.name +extension ImportedEnumCase: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(self)) + } + public static func == (lhs: ImportedEnumCase, rhs: ImportedEnumCase) -> Bool { + return lhs === rhs } } @@ -60,7 +130,6 @@ public final class ImportedFunc: ImportedDecl, CustomStringConvertible { self.swiftDecl.signatureString } - var parentType: SwiftType? { guard let selfParameter = functionSignature.selfParameter else { return nil @@ -75,6 +144,29 @@ public final class ImportedFunc: ImportedDecl, CustomStringConvertible { } } + /// If this function type uses types that require any additional `import` statements, + /// these would be exported here. + var additionalJavaImports: Set { + var imports: Set = [] +// imports += self.functionSignature.parameters.flatMap { $0.additionalJavaImports } +// imports += self.functionSignature.result.additionalJavaImports + return imports + } + + var isStatic: Bool { + if case .staticMethod = functionSignature.selfParameter { + return true + } + return false + } + + var isInitializer: Bool { + if case .initializer = functionSignature.selfParameter { + return true + } + return false + } + /// If this function/method is member of a class/struct/protocol, /// this will contain that declaration's imported name. /// @@ -87,7 +179,10 @@ public final class ImportedFunc: ImportedDecl, CustomStringConvertible { let prefix = switch self.apiKind { case .getter: "getter:" case .setter: "setter:" + case .enumCase: "case:" case .function, .initializer: "" + case .subscriptGetter: "subscriptGetter:" + case .subscriptSetter: "subscriptSetter:" } let context = if let parentType { @@ -99,6 +194,14 @@ public final class ImportedFunc: ImportedDecl, CustomStringConvertible { return prefix + context + self.name } + var isThrowing: Bool { + self.functionSignature.effectSpecifiers.contains(.throws) + } + + var isAsync: Bool { + self.functionSignature.isAsync + } + init( module: String, swiftDecl: any DeclSyntaxProtocol, @@ -133,3 +236,40 @@ extension ImportedFunc: Hashable { return lhs === rhs } } + +extension ImportedFunc { + var javaGetterName: String { + let returnsBoolean = self.functionSignature.result.type.asNominalTypeDeclaration?.knownTypeKind == .bool + + if !returnsBoolean { + return "get\(self.name.firstCharacterUppercased)" + } else if !self.name.hasJavaBooleanNamingConvention { + return "is\(self.name.firstCharacterUppercased)" + } else { + return self.name + } + } + + var javaSetterName: String { + let isBooleanSetter = self.functionSignature.parameters.first?.type.asNominalTypeDeclaration?.knownTypeKind == .bool + + // If the variable is already named "isX", then we make + // the setter "setX" to match beans spec. + if isBooleanSetter && self.name.hasJavaBooleanNamingConvention { + // Safe to force unwrap due to `hasJavaBooleanNamingConvention` check. + let propertyName = self.name.split(separator: "is", maxSplits: 1).last! + return "set\(propertyName)" + } else { + return "set\(self.name.firstCharacterUppercased)" + } + } +} + +extension ImportedNominalType: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(self)) + } + public static func == (lhs: ImportedNominalType, rhs: ImportedNominalType) -> Bool { + return lhs === rhs + } +} diff --git a/Sources/JExtractSwiftLib/JNI/JNICaching.swift b/Sources/JExtractSwiftLib/JNI/JNICaching.swift new file mode 100644 index 000000000..b0521946d --- /dev/null +++ b/Sources/JExtractSwiftLib/JNI/JNICaching.swift @@ -0,0 +1,31 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +enum JNICaching { + static func cacheName(for type: ImportedNominalType) -> String { + cacheName(for: type.swiftNominal.qualifiedName) + } + + static func cacheName(for type: SwiftNominalType) -> String { + cacheName(for: type.nominalTypeDecl.qualifiedName) + } + + private static func cacheName(for qualifiedName: String) -> String { + "_JNI_\(qualifiedName.replacingOccurrences(of: ".", with: "_"))" + } + + static func cacheMemberName(for enumCase: ImportedEnumCase) -> String { + "\(enumCase.enumType.nominalTypeDecl.name.firstCharacterLowercased)\(enumCase.name.firstCharacterUppercased)Cache" + } +} diff --git a/Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift b/Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift new file mode 100644 index 000000000..9bd9a7d83 --- /dev/null +++ b/Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift @@ -0,0 +1,58 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024-2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JavaTypes +import SwiftJavaConfigurationShared + +enum JNIJavaTypeTranslator { + + static func translate(knownType: SwiftKnownTypeDeclKind, config: Configuration) -> JavaType? { + let unsigned = config.effectiveUnsignedNumbersMode + guard unsigned == .annotate else { + // We do not support wrap mode in JNI mode currently; + // In the future this is where it would be interesting to implement Kotlin UInt support. + return nil + } + + switch knownType { + case .bool: return .boolean + + case .int8: return .byte + case .uint8: return .byte + + case .int16: return .short + case .uint16: return .char + + case .int32: return .int + case .uint32: return .int + + case .int64: return .long + case .uint64: return .long + + case .float: return .float + case .double: return .double + case .void: return .void + + case .string: return .javaLangString + + case .int, .uint, // FIXME: why not supported int/uint? + .unsafeRawPointer, .unsafeMutableRawPointer, + .unsafePointer, .unsafeMutablePointer, + .unsafeRawBufferPointer, .unsafeMutableRawBufferPointer, + .unsafeBufferPointer, .unsafeMutableBufferPointer, + .optional, .foundationData, .foundationDataProtocol, .essentialsData, .essentialsDataProtocol, .array: + return nil + } + } +} diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift new file mode 100644 index 000000000..f1e7c6851 --- /dev/null +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift @@ -0,0 +1,342 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JavaTypes +import SwiftJavaConfigurationShared +import SwiftSyntax + +extension JNISwift2JavaGenerator { + + func generateInterfaceWrappers( + _ types: [ImportedNominalType] + ) -> [ImportedNominalType: JavaInterfaceSwiftWrapper] { + var wrappers = [ImportedNominalType: JavaInterfaceSwiftWrapper]() + + for type in types { + do { + let translator = JavaInterfaceProtocolWrapperGenerator() + wrappers[type] = try translator.generate(for: type) + } catch { + self.logger.warning("Failed to generate protocol wrapper for: '\(type.swiftNominal.qualifiedName)'; \(error)") + } + } + + return wrappers + } + + /// A type that describes a Swift protocol + /// that uses an underlying wrap-java `@JavaInterface` + /// to make callbacks to Java from Swift using protocols. + struct JavaInterfaceSwiftWrapper { + let protocolType: SwiftNominalType + let functions: [Function] + let variables: [Variable] + + var wrapperName: String { + protocolType.nominalTypeDecl.javaInterfaceSwiftProtocolWrapperName + } + + var swiftName: String { + protocolType.nominalTypeDecl.qualifiedName + } + + var javaInterfaceVariableName: String { + protocolType.nominalTypeDecl.javaInterfaceVariableName + } + + var javaInterfaceName: String { + protocolType.nominalTypeDecl.javaInterfaceName + } + + struct Function { + let swiftFunctionName: String + let originalFunctionSignature: SwiftFunctionSignature + let swiftDecl: any DeclSyntaxProtocol + let parameterConversions: [UpcallConversionStep] + let resultConversion: UpcallConversionStep + } + + struct Variable { + let swiftDecl: any DeclSyntaxProtocol + let getter: Function + let setter: Function? + } + } + + + struct JavaInterfaceProtocolWrapperGenerator { + func generate(for type: ImportedNominalType) throws -> JavaInterfaceSwiftWrapper { + let functions = try type.methods.map { method in + try translate(function: method) + } + + // FIXME: Finish support for variables + if !type.variables.isEmpty { + throw JavaTranslationError.protocolVariablesNotSupported + } + + let variables = try Dictionary(grouping: type.variables, by: { $0.swiftDecl.id }).map { (id, funcs) in + precondition(funcs.count > 0 && funcs.count <= 2, "Variables must contain a getter and optionally a setter") + guard let getter = funcs.first(where: { $0.apiKind == .getter }) else { + fatalError("Getter not found for variable with imported funcs: \(funcs)") + } + let setter = funcs.first(where: { $0.apiKind == .setter }) + + return try self.translateVariable(getter: getter, setter: setter) + } + + return JavaInterfaceSwiftWrapper( + protocolType: SwiftNominalType(nominalTypeDecl: type.swiftNominal), + functions: functions, + variables: variables + ) + } + + private func translate(function: ImportedFunc) throws -> JavaInterfaceSwiftWrapper.Function { + let parameters = try function.functionSignature.parameters.map { + try self.translateParameter($0) + } + + let result = try translateResult(function.functionSignature.result, methodName: function.name) + + return JavaInterfaceSwiftWrapper.Function( + swiftFunctionName: function.name, + originalFunctionSignature: function.functionSignature, + swiftDecl: function.swiftDecl, + parameterConversions: parameters, + resultConversion: result + ) + } + + private func translateVariable(getter: ImportedFunc, setter: ImportedFunc?) throws -> JavaInterfaceSwiftWrapper.Variable { + return try JavaInterfaceSwiftWrapper.Variable( + swiftDecl: getter.swiftDecl, // they should be the same + getter: translate(function: getter), + setter: setter.map { try self.translate(function: $0) } + ) + } + + private func translateParameter(_ parameter: SwiftParameter) throws -> UpcallConversionStep { + try self.translateParameter(parameterName: parameter.parameterName!, type: parameter.type) + } + + private func translateParameter(parameterName: String, type: SwiftType) throws -> UpcallConversionStep { + + switch type { + case .nominal(let nominalType): + if let knownType = nominalType.nominalTypeDecl.knownTypeKind { + switch knownType { + case .optional: + guard let genericArgs = nominalType.genericArguments, genericArgs.count == 1 else { + throw JavaTranslationError.unsupportedSwiftType(type) + } + return try translateOptionalParameter( + name: parameterName, + wrappedType: genericArgs[0] + ) + + default: + guard knownType.isDirectlyTranslatedToWrapJava else { + throw JavaTranslationError.unsupportedSwiftType(type) + } + + return .placeholder + } + } + + // We assume this is then a JExtracted Swift class + return .toJavaWrapper( + .placeholder, + name: parameterName, + nominalType: nominalType + ) + + case .tuple([]): // void + return .placeholder + + case .optional(let wrappedType): + return try translateOptionalParameter( + name: parameterName, + wrappedType: wrappedType + ) + + case .genericParameter, .function, .metatype, .tuple, .existential, .opaque, .composite, .array: + throw JavaTranslationError.unsupportedSwiftType(type) + } + } + + private func translateOptionalParameter(name: String, wrappedType: SwiftType) throws -> UpcallConversionStep { + let wrappedConversion = try translateParameter(parameterName: name, type: wrappedType) + return .toJavaOptional(.map(.placeholder, body: wrappedConversion)) + } + + private func translateResult(_ result: SwiftResult, methodName: String) throws -> UpcallConversionStep { + try self.translateResult(type: result.type, methodName: methodName) + } + + private func translateResult( + type: SwiftType, + methodName: String, + allowNilForObjects: Bool = false + ) throws -> UpcallConversionStep { + switch type { + case .nominal(let nominalType): + if let knownType = nominalType.nominalTypeDecl.knownTypeKind { + switch knownType { + case .optional: + guard let genericArgs = nominalType.genericArguments, genericArgs.count == 1 else { + throw JavaTranslationError.unsupportedSwiftType(type) + } + return try self.translateOptionalResult( + wrappedType: genericArgs[0], + methodName: methodName + ) + + default: + guard knownType.isDirectlyTranslatedToWrapJava else { + throw JavaTranslationError.unsupportedSwiftType(type) + } + return .placeholder + } + } + + let inner: UpcallConversionStep = !allowNilForObjects ? + .unwrapOptional(.placeholder, message: "Upcall to \(methodName) unexpectedly returned nil") + : .placeholder + + // We assume this is then a JExtracted Swift class + return .toSwiftClass( + inner, + name: "result$", + nominalType: nominalType + ) + + case .tuple([]): // void + return .placeholder + + case .optional(let wrappedType): + return try self.translateOptionalResult(wrappedType: wrappedType, methodName: methodName) + + case .genericParameter, .function, .metatype, .tuple, .existential, .opaque, .composite, .array: + throw JavaTranslationError.unsupportedSwiftType(type) + } + } + + private func translateOptionalResult(wrappedType: SwiftType, methodName: String) throws -> UpcallConversionStep { + // The `fromJavaOptional` will handle the nullability + let wrappedConversion = try translateResult( + type: wrappedType, + methodName: methodName, + allowNilForObjects: true + ) + return .map(.fromJavaOptional(.placeholder), body: wrappedConversion) + } + } +} + + /// Describes how to convert values from and to wrap-java types + enum UpcallConversionStep { + case placeholder + + case constant(String) + + indirect case toJavaWrapper( + UpcallConversionStep, + name: String, + nominalType: SwiftNominalType + ) + + indirect case toSwiftClass( + UpcallConversionStep, + name: String, + nominalType: SwiftNominalType + ) + + indirect case unwrapOptional( + UpcallConversionStep, + message: String + ) + + indirect case toJavaOptional(UpcallConversionStep) + + indirect case fromJavaOptional(UpcallConversionStep) + + indirect case map(UpcallConversionStep, body: UpcallConversionStep) + + /// Returns the conversion string applied to the placeholder. + func render(_ printer: inout CodePrinter, _ placeholder: String) -> String { + switch self { + case .placeholder: + return placeholder + + case .constant(let constant): + return constant + + case .toJavaWrapper(let inner, let name, let nominalType): + let inner = inner.render(&printer, placeholder) + printer.print( + """ + let \(name)Class = try! JavaClass<\(nominalType.nominalTypeDecl.generatedJavaClassMacroName)>(environment: JavaVirtualMachine.shared().environment()) + let \(name)Pointer = UnsafeMutablePointer<\(nominalType.nominalTypeDecl.qualifiedName)>.allocate(capacity: 1) + \(name)Pointer.initialize(to: \(inner)) + """ + ) + + return "\(name)Class.wrapMemoryAddressUnsafe(Int64(Int(bitPattern: \(name)Pointer)))" + + case .toSwiftClass(let inner, let name, let nominalType): + let inner = inner.render(&printer, placeholder) + + // The wrap-java methods will return null + printer.print( + """ + let \(name)MemoryAddress$ = \(inner).as(JavaJNISwiftInstance.self)!.memoryAddress() + let \(name)Pointer = UnsafeMutablePointer<\(nominalType.nominalTypeDecl.qualifiedName)>(bitPattern: Int(\(name)MemoryAddress$))! + """ + ) + + return "\(name)Pointer.pointee" + + case .unwrapOptional(let inner, let message): + let inner = inner.render(&printer, placeholder) + + printer.print( + """ + guard let unwrapped$ = \(inner) else { + fatalError("\(message)") + } + """ + ) + + return "unwrapped$" + + case .toJavaOptional(let inner): + let inner = inner.render(&printer, placeholder) + return "\(inner).toJavaOptional()" + + case .fromJavaOptional(let inner): + let inner = inner.render(&printer, placeholder) + return "Optional(javaOptional: \(inner))" + + case .map(let inner, let body): + let inner = inner.render(&printer, placeholder) + var printer = CodePrinter() + printer.printBraceBlock("\(inner).map") { printer in + let body = body.render(&printer, "$0") + printer.print("return \(body)") + } + return printer.finalize() + } + } +} diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift new file mode 100644 index 000000000..bfa2ff12d --- /dev/null +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -0,0 +1,703 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation +import JavaTypes +import OrderedCollections + +// MARK: Defaults + +extension JNISwift2JavaGenerator { + /// Default set Java imports for every generated file + static let defaultJavaImports: Array = [ + "org.swift.swiftkit.core.*", + "org.swift.swiftkit.core.util.*", + "java.util.*", + "java.util.concurrent.atomic.AtomicBoolean", + + // NonNull, Unsigned and friends + "org.swift.swiftkit.core.annotations.*" + ] +} + +// MARK: Printing + +extension JNISwift2JavaGenerator { + func writeExportedJavaSources() throws { + var printer = CodePrinter() + try writeExportedJavaSources(&printer) + } + + package func writeExportedJavaSources(_ printer: inout CodePrinter) throws { + let importedTypes = analysis.importedTypes.sorted(by: { (lhs, rhs) in lhs.key < rhs.key }) + + var exportedFileNames: OrderedSet = [] + + // Each parent type goes into its own file + // any nested types are printed inside the body as `static class` + for (_, ty) in importedTypes.filter({ _, type in type.parent == nil }) { + let filename = "\(ty.swiftNominal.name).java" + logger.debug("Printing contents: \(filename)") + printImportedNominal(&printer, ty) + + if let outputFile = try printer.writeContents( + outputDirectory: javaOutputDirectory, + javaPackagePath: javaPackagePath, + filename: filename + ) { + exportedFileNames.append(outputFile.path(percentEncoded: false)) + logger.info("[swift-java] Generated: \(ty.swiftNominal.name.bold).java (at \(outputFile))") + } + } + + let filename = "\(self.swiftModuleName).java" + logger.trace("Printing module class: \(filename)") + printModule(&printer) + + if let outputFile = try printer.writeContents( + outputDirectory: javaOutputDirectory, + javaPackagePath: javaPackagePath, + filename: filename + ) { + exportedFileNames.append(outputFile.path(percentEncoded: false)) + logger.info("[swift-java] Generated: \(self.swiftModuleName).java (at \(outputFile))") + } + + // Write java sources list file + if let generatedJavaSourcesListFileOutput = config.generatedJavaSourcesListFileOutput, !exportedFileNames.isEmpty { + let outputPath = URL(fileURLWithPath: javaOutputDirectory).appending(path: generatedJavaSourcesListFileOutput) + try exportedFileNames.joined(separator: "\n").write( + to: outputPath, + atomically: true, + encoding: .utf8 + ) + logger.info("Generated file at \(outputPath)") + } + } + + + + private func printModule(_ printer: inout CodePrinter) { + printHeader(&printer) + printPackage(&printer) + printImports(&printer) + + printModuleClass(&printer) { printer in + printer.print( + """ + static final String LIB_NAME = "\(swiftModuleName)"; + + static { + System.loadLibrary(LIB_NAME); + } + """ + ) + + for decl in analysis.importedGlobalFuncs { + self.logger.trace("Print global function: \(decl)") + printFunctionDowncallMethods(&printer, decl) + printer.println() + } + + for decl in analysis.importedGlobalVariables { + self.logger.trace("Print global variable: \(decl)") + printFunctionDowncallMethods(&printer, decl) + printer.println() + } + } + } + + private func printImportedNominal(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + printHeader(&printer) + printPackage(&printer) + printImports(&printer) + + switch decl.swiftNominal.kind { + case .actor, .class, .enum, .struct: + printConcreteType(&printer, decl) + case .protocol: + printProtocol(&printer, decl) + } + } + + private func printProtocol(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + var extends = [String]() + + // If we cannot generate Swift wrappers + // that allows the user to implement the wrapped interface in Java + // then we require only JExtracted types can conform to this. + if !self.interfaceProtocolWrappers.keys.contains(decl) { + extends.append("JNISwiftInstance") + } + let extendsString = extends.isEmpty ? "" : " extends \(extends.joined(separator: ", "))" + + printer.printBraceBlock("public interface \(decl.swiftNominal.name)\(extendsString)") { printer in + for initializer in decl.initializers { + printFunctionDowncallMethods(&printer, initializer, skipMethodBody: true) + printer.println() + } + + for method in decl.methods { + printFunctionDowncallMethods(&printer, method, skipMethodBody: true) + printer.println() + } + + for variable in decl.variables { + printFunctionDowncallMethods(&printer, variable, skipMethodBody: true) + printer.println() + } + } + } + + private func printConcreteType(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + printNominal(&printer, decl) { printer in + printer.print( + """ + static final String LIB_NAME = "\(swiftModuleName)"; + + @SuppressWarnings("unused") + private static final boolean INITIALIZED_LIBS = initializeLibs(); + static boolean initializeLibs() { + System.loadLibrary(LIB_NAME); + return true; + } + """ + ) + + let nestedTypes = self.analysis.importedTypes.filter { _, type in + type.parent == decl.swiftNominal + } + + for nestedType in nestedTypes { + printConcreteType(&printer, nestedType.value) + printer.println() + } + + printer.print( + """ + /** + * The designated constructor of any imported Swift types. + * + * @param selfPointer a pointer to the memory containing the value + * @param swiftArena the arena this object belongs to. When the arena goes out of scope, this value is destroyed. + */ + private \(decl.swiftNominal.name)(long selfPointer, SwiftArena swiftArena) { + SwiftObjects.requireNonZero(selfPointer, "selfPointer"); + this.selfPointer = selfPointer; + + // Only register once we have fully initialized the object since this will need the object pointer. + swiftArena.register(this); + } + + /** + * Assume that the passed {@code long} represents a memory address of a {@link \(decl.swiftNominal.name)}. + *

+ * Warnings: + *

    + *
  • No checks are performed about the compatibility of the pointed at memory and the actual \(decl.swiftNominal.name) types.
  • + *
  • This operation does not copy, or retain, the pointed at pointer, so its lifetime must be ensured manually to be valid when wrapping.
  • + *
+ */ + public static \(decl.swiftNominal.name) wrapMemoryAddressUnsafe(long selfPointer, SwiftArena swiftArena) { + return new \(decl.swiftNominal.name)(selfPointer, swiftArena); + } + + public static \(decl.swiftNominal.name) wrapMemoryAddressUnsafe(long selfPointer) { + return new \(decl.swiftNominal.name)(selfPointer, SwiftMemoryManagement.GLOBAL_SWIFT_JAVA_ARENA); + } + """ + ) + + printer.print( + """ + /** Pointer to the "self". */ + private final long selfPointer; + + /** Used to track additional state of the underlying object, e.g. if it was explicitly destroyed. */ + private final AtomicBoolean $state$destroyed = new AtomicBoolean(false); + + public long $memoryAddress() { + return this.selfPointer; + } + + @Override + public AtomicBoolean $statusDestroyedFlag() { + return $state$destroyed; + } + """ + ) + + printer.println() + + if decl.swiftNominal.kind == .enum { + printEnumHelpers(&printer, decl) + printer.println() + } + + for initializer in decl.initializers { + printFunctionDowncallMethods(&printer, initializer) + printer.println() + } + + for method in decl.methods { + printFunctionDowncallMethods(&printer, method) + printer.println() + } + + for variable in decl.variables { + printFunctionDowncallMethods(&printer, variable) + printer.println() + } + + printToStringMethods(&printer, decl) + printer.println() + + printTypeMetadataAddressFunction(&printer, decl) + printer.println() + printDestroyFunction(&printer, decl) + } + } + + + private func printToStringMethods(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + printer.printBraceBlock("public String toString()") { printer in + printer.print( + """ + return $toString(this.$memoryAddress()); + """ + ) + } + printer.print("private static native java.lang.String $toString(long selfPointer);") + + printer.println() + + printer.printBraceBlock("public String toDebugString()") { printer in + printer.print( + """ + return $toDebugString(this.$memoryAddress()); + """ + ) + } + printer.print("private static native java.lang.String $toDebugString(long selfPointer);") + } + + private func printHeader(_ printer: inout CodePrinter) { + printer.print( + """ + // Generated by jextract-swift + // Swift module: \(swiftModuleName) + + """ + ) + } + + private func printPackage(_ printer: inout CodePrinter) { + printer.print( + """ + package \(javaPackage); + + """ + ) + } + + private func printImports(_ printer: inout CodePrinter) { + for i in JNISwift2JavaGenerator.defaultJavaImports { + printer.print("import \(i);") + } + printer.print("") + } + + private func printNominal( + _ printer: inout CodePrinter, _ decl: ImportedNominalType, body: (inout CodePrinter) -> Void + ) { + if decl.swiftNominal.isSendable { + printer.print("@ThreadSafe // Sendable") + } + var modifiers = ["public"] + if decl.parent != nil { + modifiers.append("static") + } + modifiers.append("final") + var implements = ["JNISwiftInstance"] + implements += decl.inheritedTypes + .compactMap(\.asNominalTypeDeclaration) + .filter { $0.kind == .protocol } + .map(\.name) + let implementsClause = implements.joined(separator: ", ") + printer.printBraceBlock("\(modifiers.joined(separator: " ")) class \(decl.swiftNominal.name) implements \(implementsClause)") { printer in + body(&printer) + } + } + + private func printModuleClass(_ printer: inout CodePrinter, body: (inout CodePrinter) -> Void) { + printer.printBraceBlock("public final class \(swiftModuleName)") { printer in + body(&printer) + } + } + + private func printEnumHelpers(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + printEnumDiscriminator(&printer, decl) + printer.println() + printEnumCaseInterface(&printer, decl) + printer.println() + printEnumStaticInitializers(&printer, decl) + printer.println() + printEnumCases(&printer, decl) + } + + private func printEnumDiscriminator(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + printer.printBraceBlock("public enum Discriminator") { printer in + printer.print( + decl.cases.map { $0.name.uppercased() }.joined(separator: ",\n") + ) + } + + // TODO: Consider whether all of these "utility" functions can be printed using our existing printing logic. + printer.printBraceBlock("public Discriminator getDiscriminator()") { printer in + printer.print("return Discriminator.values()[$getDiscriminator(this.$memoryAddress())];") + } + printer.print("private static native int $getDiscriminator(long self);") + } + + private func printEnumCaseInterface(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + printer.print("public sealed interface Case {}") + printer.println() + + let requiresSwiftArena = decl.cases.compactMap { + self.translatedEnumCase(for: $0) + }.contains(where: \.requiresSwiftArena) + + printer.printBraceBlock("public Case getCase(\(requiresSwiftArena ? "SwiftArena swiftArena$" : ""))") { printer in + printer.print("Discriminator discriminator = this.getDiscriminator();") + printer.printBraceBlock("switch (discriminator)") { printer in + for enumCase in decl.cases { + guard let translatedCase = self.translatedEnumCase(for: enumCase) else { + continue + } + let arenaArgument = translatedCase.requiresSwiftArena ? "swiftArena$" : "" + printer.print("case \(enumCase.name.uppercased()): return this.getAs\(enumCase.name.firstCharacterUppercased)(\(arenaArgument)).orElseThrow();") + } + } + printer.print(#"throw new RuntimeException("Unknown discriminator value " + discriminator);"#) + } + } + + private func printEnumStaticInitializers(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + for enumCase in decl.cases { + printFunctionDowncallMethods(&printer, enumCase.caseFunction) + } + } + + private func printEnumCases(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + for enumCase in decl.cases { + guard let translatedCase = self.translatedEnumCase(for: enumCase) else { + return + } + + let members = translatedCase.translatedValues.map { + $0.parameter.renderParameter() + } + + let caseName = enumCase.name.firstCharacterUppercased + + // Print record + printer.printBraceBlock("public record \(caseName)(\(members.joined(separator: ", "))) implements Case") { printer in + let nativeParameters = zip(translatedCase.translatedValues, translatedCase.parameterConversions).flatMap { value, conversion in + ["\(conversion.native.javaType) \(value.parameter.name)"] + } + + printer.print("record _NativeParameters(\(nativeParameters.joined(separator: ", "))) {}") + } + + self.printJavaBindingWrapperMethod(&printer, translatedCase.getAsCaseFunction, skipMethodBody: false) + printer.println() + } + } + + private func printFunctionDowncallMethods( + _ printer: inout CodePrinter, + _ decl: ImportedFunc, + skipMethodBody: Bool = false + ) { + guard translatedDecl(for: decl) != nil else { + // Failed to translate. Skip. + return + } + + printer.printSeparator(decl.displayName) + + printJavaBindingWrapperHelperClass(&printer, decl) + + printJavaBindingWrapperMethod(&printer, decl, skipMethodBody: skipMethodBody) + } + + /// Print the helper type container for a user-facing Java API. + /// + /// * User-facing functional interfaces. + private func printJavaBindingWrapperHelperClass( + _ printer: inout CodePrinter, + _ decl: ImportedFunc + ) { + let translated = self.translatedDecl(for: decl)! + if translated.functionTypes.isEmpty { + return + } + + printer.printBraceBlock( + """ + public static class \(translated.name) + """ + ) { printer in + for functionType in translated.functionTypes { + printJavaBindingWrapperFunctionTypeHelper(&printer, functionType) + } + } + } + + /// Print "wrapper" functional interface representing a Swift closure type. + func printJavaBindingWrapperFunctionTypeHelper( + _ printer: inout CodePrinter, + _ functionType: TranslatedFunctionType + ) { + let apiParams = functionType.parameters.map({ $0.parameter.renderParameter() }) + + printer.print( + """ + @FunctionalInterface + public interface \(functionType.name) { + \(functionType.result.javaType) apply(\(apiParams.joined(separator: ", "))); + } + """ + ) + } + + private func printJavaBindingWrapperMethod( + _ printer: inout CodePrinter, + _ decl: ImportedFunc, + skipMethodBody: Bool + ) { + guard let translatedDecl = translatedDecl(for: decl) else { + fatalError("Decl was not translated, \(decl)") + } + printJavaBindingWrapperMethod(&printer, translatedDecl, importedFunc: decl, skipMethodBody: skipMethodBody) + } + + private func printJavaBindingWrapperMethod( + _ printer: inout CodePrinter, + _ translatedDecl: TranslatedFunctionDecl, + importedFunc: ImportedFunc? = nil, + skipMethodBody: Bool + ) { + var modifiers = ["public"] + if translatedDecl.isStatic { + modifiers.append("static") + } + + let translatedSignature = translatedDecl.translatedFunctionSignature + let resultType = translatedSignature.resultType.javaType + var parameters = translatedDecl.translatedFunctionSignature.parameters.map { $0.parameter.renderParameter() } + let throwsClause = translatedDecl.isThrowing && !translatedDecl.isAsync ? " throws Exception" : "" + + let generics = translatedDecl.translatedFunctionSignature.parameters.reduce(into: [(String, [JavaType])]()) { generics, parameter in + guard case .generic(let name, let extends) = parameter.parameter.type else { + return + } + generics.append((name, extends)) + } + .map { "\($0) extends \($1.compactMap(\.className).joined(separator: " & "))" } + .joined(separator: ", ") + + if !generics.isEmpty { + modifiers.append("<" + generics + ">") + } + + var annotationsStr = translatedSignature.annotations.map({ $0.render() }).joined(separator: "\n") + if !annotationsStr.isEmpty { annotationsStr += "\n" } + + let parametersStr = parameters.joined(separator: ", ") + + // Print default global arena variation + // If we have enabled javaCallbacks we must emit default + // arena methods for protocols, as this is what + // Swift will call into, when you call a interface from Swift. + let shouldGenerateGlobalArenaVariation: Bool + let isParentProtocol = importedFunc?.parentType?.asNominalType?.isProtocol ?? false + + if config.effectiveMemoryManagementMode.requiresGlobalArena && translatedSignature.requiresSwiftArena { + shouldGenerateGlobalArenaVariation = true + } else if isParentProtocol, translatedSignature.requiresSwiftArena, config.effectiveEnableJavaCallbacks { + shouldGenerateGlobalArenaVariation = true + } else { + shouldGenerateGlobalArenaVariation = false + } + + if shouldGenerateGlobalArenaVariation { + if let importedFunc { + printDeclDocumentation(&printer, importedFunc) + } + var modifiers = modifiers + + // If we are a protocol, we emit this as default method + if isParentProtocol { + modifiers.insert("default", at: 1) + } + + printer.printBraceBlock("\(annotationsStr)\(modifiers.joined(separator: " ")) \(resultType) \(translatedDecl.name)(\(parametersStr))\(throwsClause)") { printer in + let globalArenaName = "SwiftMemoryManagement.GLOBAL_SWIFT_JAVA_ARENA" + let arguments = translatedDecl.translatedFunctionSignature.parameters.map(\.parameter.name) + [globalArenaName] + let call = "\(translatedDecl.name)(\(arguments.joined(separator: ", ")))" + if translatedDecl.translatedFunctionSignature.resultType.javaType.isVoid { + printer.print("\(call);") + } else { + printer.print("return \(call);") + } + } + printer.println() + } + + if translatedSignature.requiresSwiftArena { + parameters.append("SwiftArena swiftArena$") + } + if let importedFunc { + printDeclDocumentation(&printer, importedFunc) + } + let signature = "\(annotationsStr)\(modifiers.joined(separator: " ")) \(resultType) \(translatedDecl.name)(\(parameters.joined(separator: ", ")))\(throwsClause)" + if skipMethodBody { + printer.print("\(signature);") + } else { + printer.printBraceBlock(signature) { printer in + printDowncall(&printer, translatedDecl) + } + + printNativeFunction(&printer, translatedDecl) + } + } + + private func printNativeFunction(_ printer: inout CodePrinter, _ translatedDecl: TranslatedFunctionDecl) { + let nativeSignature = translatedDecl.nativeFunctionSignature + let resultType = nativeSignature.result.javaType + var parameters = nativeSignature.parameters.flatMap(\.parameters) + if let selfParameter = nativeSignature.selfParameter?.parameters { + parameters += selfParameter + } + parameters += nativeSignature.result.outParameters + + let renderedParameters = parameters.map { javaParameter in + "\(javaParameter.type) \(javaParameter.name)" + }.joined(separator: ", ") + + printer.print("private static native \(resultType) \(translatedDecl.nativeFunctionName)(\(renderedParameters));") + } + + private func printDowncall( + _ printer: inout CodePrinter, + _ translatedDecl: TranslatedFunctionDecl + ) { + let translatedFunctionSignature = translatedDecl.translatedFunctionSignature + + // Regular parameters. + var arguments = [String]() + for parameter in translatedFunctionSignature.parameters { + let lowered = parameter.conversion.render(&printer, parameter.parameter.name) + arguments.append(lowered) + } + + // 'self' parameter. + if let selfParameter = translatedFunctionSignature.selfParameter { + let lowered = selfParameter.conversion.render(&printer, "this") + arguments.append(lowered) + } + + // Indirect return receivers + for outParameter in translatedFunctionSignature.resultType.outParameters { + printer.print("\(outParameter.type) \(outParameter.name) = \(outParameter.allocation.render(type: outParameter.type));") + arguments.append(outParameter.name) + } + + //=== Part 3: Downcall. + // TODO: If we always generate a native method and a "public" method, we can actually choose our own thunk names + // using the registry? + let downcall = "\(translatedDecl.parentName).\(translatedDecl.nativeFunctionName)(\(arguments.joined(separator: ", ")))" + + //=== Part 4: Convert the return value. + if translatedFunctionSignature.resultType.javaType.isVoid { + printer.print("\(downcall);") + } else { + let result = translatedFunctionSignature.resultType.conversion.render(&printer, downcall) + printer.print("return \(result);") + } + } + + private func printDeclDocumentation(_ printer: inout CodePrinter, _ decl: ImportedFunc) { + printer.print( + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * \(decl.signatureString) + * } + */ + """ + ) + } + + private func printTypeMetadataAddressFunction(_ printer: inout CodePrinter, _ type: ImportedNominalType) { + printer.print("private static native long $typeMetadataAddressDowncall();") + + let funcName = "$typeMetadataAddress" + printer.print("@Override") + printer.printBraceBlock("public long $typeMetadataAddress()") { printer in + printer.print( + """ + long self$ = this.$memoryAddress(); + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall("\(type.swiftNominal.name).\(funcName)", + "this", this, + "self", self$); + } + return \(type.swiftNominal.name).$typeMetadataAddressDowncall(); + """ + ) + } + } + + /// Prints the destroy function for a `JNISwiftInstance` + private func printDestroyFunction(_ printer: inout CodePrinter, _ type: ImportedNominalType) { + printer.print("private static native void $destroy(long selfPointer);") + + let funcName = "$createDestroyFunction" + printer.print("@Override") + printer.printBraceBlock("public Runnable \(funcName)()") { printer in + printer.print( + """ + long self$ = this.$memoryAddress(); + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall("\(type.swiftNominal.name).\(funcName)", + "this", this, + "self", self$); + } + return new Runnable() { + @Override + public void run() { + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall("\(type.swiftNominal.name).$destroy", "self", self$); + } + \(type.swiftNominal.name).$destroy(self$); + } + }; + """ + ) + } + } +} diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift new file mode 100644 index 000000000..553b3e10b --- /dev/null +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -0,0 +1,1347 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JavaTypes +import SwiftJavaConfigurationShared + +extension JNISwift2JavaGenerator { + var javaTranslator: JavaTranslation { + JavaTranslation( + config: config, + swiftModuleName: swiftModuleName, + javaPackage: self.javaPackage, + javaClassLookupTable: self.javaClassLookupTable, + knownTypes: SwiftKnownTypes(symbolTable: lookupContext.symbolTable), + protocolWrappers: self.interfaceProtocolWrappers + ) + } + + func translatedDecl( + for decl: ImportedFunc + ) -> TranslatedFunctionDecl? { + if let cached = translatedDecls[decl] { + return cached + } + + let translated: TranslatedFunctionDecl? + do { + translated = try self.javaTranslator.translate(decl) + } catch { + self.logger.debug("Failed to translate: '\(decl.swiftDecl.qualifiedNameForDebug)'; \(error)") + translated = nil + } + + translatedDecls[decl] = translated + return translated + } + + func translatedEnumCase( + for decl: ImportedEnumCase + ) -> TranslatedEnumCase? { + if let cached = translatedEnumCases[decl] { + return cached + } + + let translated: TranslatedEnumCase? + do { + let translation = JavaTranslation( + config: config, + swiftModuleName: swiftModuleName, + javaPackage: self.javaPackage, + javaClassLookupTable: self.javaClassLookupTable, + knownTypes: SwiftKnownTypes(symbolTable: lookupContext.symbolTable), + protocolWrappers: self.interfaceProtocolWrappers + ) + translated = try translation.translate(enumCase: decl) + } catch { + self.logger.debug("Failed to translate: '\(decl.swiftDecl.qualifiedNameForDebug)'; \(error)") + translated = nil + } + + translatedEnumCases[decl] = translated + return translated + } + + struct JavaTranslation { + let config: Configuration + let swiftModuleName: String + let javaPackage: String + let javaClassLookupTable: JavaClassLookupTable + var knownTypes: SwiftKnownTypes + let protocolWrappers: [ImportedNominalType: JavaInterfaceSwiftWrapper] + + func translate(enumCase: ImportedEnumCase) throws -> TranslatedEnumCase { + let nativeTranslation = NativeJavaTranslation( + config: self.config, + javaPackage: self.javaPackage, + javaClassLookupTable: self.javaClassLookupTable, + knownTypes: self.knownTypes, + protocolWrappers: self.protocolWrappers + ) + + let methodName = "" // TODO: Used for closures, replace with better name? + let parentName = "" // TODO: Used for closures, replace with better name? + + let translatedValues = try self.translateParameters( + enumCase.parameters.map { ($0.name, $0.type) }, + methodName: methodName, + parentName: parentName, + genericParameters: [], + genericRequirements: [] + ) + + let conversions = try enumCase.parameters.enumerated().map { idx, parameter in + let resultName = parameter.name ?? "arg\(idx)" + let result = SwiftResult(convention: .direct, type: parameter.type) + var translatedResult = try self.translate(swiftResult: result, resultName: resultName) + translatedResult.conversion = .replacingPlaceholder(translatedResult.conversion, placeholder: "$nativeParameters.\(resultName)") + let nativeResult = try nativeTranslation.translate(swiftResult: result, resultName: resultName) + return (translated: translatedResult, native: nativeResult) + } + + let caseName = enumCase.name.firstCharacterUppercased + let enumName = enumCase.enumType.nominalTypeDecl.name + let nativeParametersType = JavaType.class(package: nil, name: "\(caseName)._NativeParameters") + let getAsCaseName = "getAs\(caseName)" + // If the case has no parameters, we can skip the native call. + let constructRecordConversion = JavaNativeConversionStep.method(.constant("Optional"), function: "of", arguments: [ + .constructJavaClass( + .commaSeparated(conversions.map(\.translated.conversion)), + .class(package: nil,name: caseName) + ) + ]) + let getAsCaseFunction = TranslatedFunctionDecl( + name: getAsCaseName, + isStatic: false, + isThrowing: false, + isAsync: false, + nativeFunctionName: "$\(getAsCaseName)", + parentName: enumName, + functionTypes: [], + translatedFunctionSignature: TranslatedFunctionSignature( + selfParameter: TranslatedParameter( + parameter: JavaParameter(name: "self", type: .long), + conversion: .aggregate( + [ + .ifStatement(.constant("getDiscriminator() != Discriminator.\(caseName.uppercased())"), thenExp: .constant("return Optional.empty();")), + .valueMemoryAddress(.placeholder) + ] + ) + ), + parameters: [], + resultType: TranslatedResult( + javaType: .class(package: nil, name: "Optional<\(caseName)>"), + outParameters: conversions.flatMap(\.translated.outParameters), + conversion: enumCase.parameters.isEmpty ? constructRecordConversion : .aggregate(variable: ("$nativeParameters", nativeParametersType), [constructRecordConversion]) + ) + ), + nativeFunctionSignature: NativeFunctionSignature( + selfParameter: NativeParameter( + parameters: [JavaParameter(name: "self", type: .long)], + conversion: .extractSwiftValue(.placeholder, swiftType: .nominal(enumCase.enumType), allowNil: false) + ), + parameters: [], + result: NativeResult( + javaType: nativeParametersType, + conversion: .placeholder, + outParameters: conversions.flatMap(\.native.outParameters) + ) + ) + ) + + return TranslatedEnumCase( + name: enumCase.name.firstCharacterUppercased, + enumName: enumCase.enumType.nominalTypeDecl.name, + original: enumCase, + translatedValues: translatedValues, + parameterConversions: conversions, + getAsCaseFunction: getAsCaseFunction + ) + } + + func translate(_ decl: ImportedFunc) throws -> TranslatedFunctionDecl { + let nativeTranslation = NativeJavaTranslation( + config: self.config, + javaPackage: self.javaPackage, + javaClassLookupTable: self.javaClassLookupTable, + knownTypes: self.knownTypes, + protocolWrappers: self.protocolWrappers + ) + + // Types with no parent will be outputted inside a "module" class. + let parentName = decl.parentType?.asNominalType?.nominalTypeDecl.qualifiedName ?? swiftModuleName + + // Name. + let javaName = switch decl.apiKind { + case .getter, .subscriptGetter: decl.javaGetterName + case .setter, .subscriptSetter: decl.javaSetterName + case .function, .initializer, .enumCase: decl.name + } + + // Swift -> Java + var translatedFunctionSignature = try translate( + functionSignature: decl.functionSignature, + methodName: javaName, + parentName: parentName + ) + // Java -> Java (native) + var nativeFunctionSignature = try nativeTranslation.translate( + functionSignature: decl.functionSignature, + translatedFunctionSignature: translatedFunctionSignature, + methodName: javaName, + parentName: parentName + ) + + // Closures. + var funcTypes: [TranslatedFunctionType] = [] + for (idx, param) in decl.functionSignature.parameters.enumerated() { + let parameterName = param.parameterName ?? "_\(idx)" + + switch param.type { + case .function(let funcTy): + let translatedClosure = try translateFunctionType( + name: parameterName, + swiftType: funcTy, + parentName: parentName + ) + funcTypes.append(translatedClosure) + default: + break + } + } + + // Handle async methods + if decl.functionSignature.isAsync { + self.convertToAsync( + translatedFunctionSignature: &translatedFunctionSignature, + nativeFunctionSignature: &nativeFunctionSignature, + originalFunctionSignature: decl.functionSignature, + mode: config.effectiveAsyncFuncMode + ) + } + + return TranslatedFunctionDecl( + name: javaName, + isStatic: decl.isStatic || !decl.hasParent || decl.isInitializer, + isThrowing: decl.isThrowing, + isAsync: decl.isAsync, + nativeFunctionName: "$\(javaName)", + parentName: parentName, + functionTypes: funcTypes, + translatedFunctionSignature: translatedFunctionSignature, + nativeFunctionSignature: nativeFunctionSignature + ) + } + + /// Translate Swift closure type to Java functional interface. + func translateFunctionType( + name: String, + swiftType: SwiftFunctionType, + parentName: String + ) throws -> TranslatedFunctionType { + var translatedParams: [TranslatedParameter] = [] + + for (i, param) in swiftType.parameters.enumerated() { + let paramName = param.parameterName ?? "_\(i)" + translatedParams.append( + try translateParameter( + swiftType: param.type, + parameterName: paramName, + methodName: name, + parentName: parentName, + genericParameters: [], + genericRequirements: [], + parameterPosition: nil + ) + ) + } + + let translatedResult = try translate(swiftResult: SwiftResult(convention: .direct, type: swiftType.resultType)) + + return TranslatedFunctionType( + name: name, + parameters: translatedParams, + result: translatedResult, + swiftType: swiftType + ) + } + + func translate( + functionSignature: SwiftFunctionSignature, + methodName: String, + parentName: String + ) throws -> TranslatedFunctionSignature { + let parameters = try translateParameters( + functionSignature.parameters.map { ($0.parameterName, $0.type )}, + methodName: methodName, + parentName: parentName, + genericParameters: functionSignature.genericParameters, + genericRequirements: functionSignature.genericRequirements + ) + + // 'self' + let selfParameter = try self.translateSelfParameter( + functionSignature.selfParameter, + methodName: methodName, + parentName: parentName, + genericParameters: functionSignature.genericParameters, + genericRequirements: functionSignature.genericRequirements + ) + + let resultType = try translate(swiftResult: functionSignature.result) + + return TranslatedFunctionSignature( + selfParameter: selfParameter, + parameters: parameters, + resultType: resultType + ) + } + + func translateParameters( + _ parameters: [(name: String?, type: SwiftType)], + methodName: String, + parentName: String, + genericParameters: [SwiftGenericParameterDeclaration], + genericRequirements: [SwiftGenericRequirement] + ) throws -> [TranslatedParameter] { + try parameters.enumerated().map { idx, param in + let parameterName = param.name ?? "arg\(idx)" + return try translateParameter( + swiftType: param.type, + parameterName: parameterName, + methodName: methodName, + parentName: parentName, + genericParameters: genericParameters, + genericRequirements: genericRequirements, + parameterPosition: idx + ) + } + } + + func translateSelfParameter( + _ selfParameter: SwiftSelfParameter?, + methodName: String, + parentName: String, + genericParameters: [SwiftGenericParameterDeclaration], + genericRequirements: [SwiftGenericRequirement] + ) throws -> TranslatedParameter? { + // 'self' + if case .instance(let swiftSelf) = selfParameter { + return try self.translateParameter( + swiftType: swiftSelf.type, + parameterName: swiftSelf.parameterName ?? "self", + methodName: methodName, + parentName: parentName, + genericParameters: genericParameters, + genericRequirements: genericRequirements, + parameterPosition: nil + ) + } else { + return nil + } + } + + func translateParameter( + swiftType: SwiftType, + parameterName: String, + methodName: String, + parentName: String, + genericParameters: [SwiftGenericParameterDeclaration], + genericRequirements: [SwiftGenericRequirement], + parameterPosition: Int? + ) throws -> TranslatedParameter { + + // If the result type should cause any annotations on the method, include them here. + let parameterAnnotations: [JavaAnnotation] = getTypeAnnotations(swiftType: swiftType, config: config) + + // If we need to handle unsigned integers do so here + if config.effectiveUnsignedNumbersMode.needsConversion { + if let unsignedWrapperType = JavaType.unsignedWrapper(for: swiftType) { + return TranslatedParameter( + parameter: JavaParameter(name: parameterName, type: unsignedWrapperType, annotations: parameterAnnotations), + conversion: unsignedResultConversion( + swiftType, to: unsignedWrapperType, + mode: self.config.effectiveUnsignedNumbersMode) + ) + } + } + + switch swiftType { + case .nominal(let nominalType): + let nominalTypeName = nominalType.nominalTypeDecl.qualifiedName + + if let knownType = nominalType.nominalTypeDecl.knownTypeKind { + switch knownType { + case .optional: + guard let genericArgs = nominalType.genericArguments, genericArgs.count == 1 else { + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + return try translateOptionalParameter( + wrappedType: genericArgs[0], + parameterName: parameterName + ) + + case .array: + guard let elementType = nominalType.genericArguments?.first else { + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + return try translateArrayParameter( + elementType: elementType, + parameterName: parameterName + ) + + default: + guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config) else { + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + + return TranslatedParameter( + parameter: JavaParameter(name: parameterName, type: javaType, annotations: parameterAnnotations), + conversion: .placeholder + ) + } + } + + if nominalType.isSwiftJavaWrapper { + guard let javaType = nominalTypeName.parseJavaClassFromSwiftJavaName(in: self.javaClassLookupTable) else { + throw JavaTranslationError.wrappedJavaClassTranslationNotProvided(swiftType) + } + + return TranslatedParameter( + parameter: JavaParameter(name: parameterName, type: javaType, annotations: parameterAnnotations), + conversion: .placeholder + ) + } + + // We assume this is a JExtract class. + return TranslatedParameter( + parameter: JavaParameter( + name: parameterName, + type: .concrete(.class(package: nil, name: nominalTypeName)), + annotations: parameterAnnotations + ), + conversion: .valueMemoryAddress(.placeholder) + ) + + case .tuple([]): + return TranslatedParameter( + parameter: JavaParameter(name: parameterName, type: .void, annotations: parameterAnnotations), + conversion: .placeholder + ) + + case .function: + return TranslatedParameter( + parameter: JavaParameter( + name: parameterName, + type: .class(package: javaPackage, name: "\(parentName).\(methodName).\(parameterName)"), + annotations: parameterAnnotations + ), + conversion: .placeholder + ) + + case .optional(let wrapped): + return try translateOptionalParameter( + wrappedType: wrapped, + parameterName: parameterName + ) + + case .opaque(let proto), .existential(let proto): + guard let parameterPosition else { + fatalError("Cannot extract opaque or existential type that is not a parameter: \(proto)") + } + + return try translateProtocolParameter( + protocolType: proto, + parameterName: parameterName, + javaGenericName: "_T\(parameterPosition)" + ) + + case .genericParameter(let generic): + if let concreteTy = swiftType.typeIn(genericParameters: genericParameters, genericRequirements: genericRequirements) { + return try translateProtocolParameter( + protocolType: concreteTy, + parameterName: parameterName, + javaGenericName: generic.name + ) + } + + throw JavaTranslationError.unsupportedSwiftType(swiftType) + + case .array(let elementType): + return try translateArrayParameter( + elementType: elementType, + parameterName: parameterName + ) + + case .metatype, .tuple, .composite: + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + } + + func unsignedResultConversion( + _ from: SwiftType, + to javaType: JavaType, + mode: JExtractUnsignedIntegerMode + ) -> JavaNativeConversionStep { + switch mode { + case .annotate: + return .placeholder // no conversions + + case .wrapGuava: + fatalError("JExtract in JNI mode does not support the \(JExtractUnsignedIntegerMode.wrapGuava) unsigned numerics mode") + } + } + + func convertToAsync( + translatedFunctionSignature: inout TranslatedFunctionSignature, + nativeFunctionSignature: inout NativeFunctionSignature, + originalFunctionSignature: SwiftFunctionSignature, + mode: JExtractAsyncFuncMode + ) { + // Update translated function + let nativeFutureType: JavaType + let translatedFutureType: JavaType + let completeMethodID: String + let completeExceptionallyMethodID: String + + switch mode { + case .completableFuture: + nativeFutureType = .completableFuture(nativeFunctionSignature.result.javaType) + translatedFutureType = .completableFuture(translatedFunctionSignature.resultType.javaType) + completeMethodID = "_JNIMethodIDCache.CompletableFuture.complete" + completeExceptionallyMethodID = "_JNIMethodIDCache.CompletableFuture.completeExceptionally" + + case .legacyFuture: + nativeFutureType = .simpleCompletableFuture(nativeFunctionSignature.result.javaType) + translatedFutureType = .future(translatedFunctionSignature.resultType.javaType) + completeMethodID = "_JNIMethodIDCache.SimpleCompletableFuture.complete" + completeExceptionallyMethodID = "_JNIMethodIDCache.SimpleCompletableFuture.completeExceptionally" + } + + let futureOutParameter = OutParameter( + name: "future$", + type: nativeFutureType, + allocation: .new + ) + + let result = translatedFunctionSignature.resultType + translatedFunctionSignature.resultType = TranslatedResult( + javaType: translatedFutureType, + annotations: result.annotations, + outParameters: result.outParameters + [futureOutParameter], + conversion: .aggregate(variable: nil, [ + .print(.placeholder), // Make the downcall + .method(.constant("future$"), function: "thenApply", arguments: [ + .lambda(args: ["futureResult$"], body: .replacingPlaceholder(result.conversion, placeholder: "futureResult$")) + ]) + ]) + ) + + // Update native function + nativeFunctionSignature.result.conversion = .asyncCompleteFuture( + swiftFunctionResultType: originalFunctionSignature.result.type, + nativeFunctionSignature: nativeFunctionSignature, + isThrowing: originalFunctionSignature.isThrowing, + completeMethodID: completeMethodID, + completeExceptionallyMethodID: completeExceptionallyMethodID + ) + nativeFunctionSignature.result.javaType = .void + nativeFunctionSignature.result.outParameters.append(.init(name: "result_future", type: nativeFutureType)) + } + + func translateProtocolParameter( + protocolType: SwiftType, + parameterName: String, + javaGenericName: String + ) throws -> TranslatedParameter { + switch protocolType { + case .nominal: + return try translateProtocolParameter(protocolTypes: [protocolType], parameterName: parameterName, javaGenericName: javaGenericName) + + case .composite(let types): + return try translateProtocolParameter(protocolTypes: types, parameterName: parameterName, javaGenericName: javaGenericName) + + default: + throw JavaTranslationError.unsupportedSwiftType(protocolType) + } + } + + private func translateProtocolParameter( + protocolTypes: [SwiftType], + parameterName: String, + javaGenericName: String + ) throws -> TranslatedParameter { + let javaProtocolTypes = try protocolTypes.map { + switch $0 { + case .nominal(let nominalType): + let nominalTypeName = nominalType.nominalTypeDecl.name + return JavaType.class(package: nil, name: nominalTypeName) + + default: + throw JavaTranslationError.unsupportedSwiftType($0) + } + } + + // We just pass down the jobject + return TranslatedParameter( + parameter: JavaParameter( + name: parameterName, + type: .generic(name: javaGenericName, extends: javaProtocolTypes), + annotations: [] + ), + conversion: .placeholder + ) + } + + func translateOptionalParameter( + wrappedType swiftType: SwiftType, + parameterName: String + ) throws -> TranslatedParameter { + let parameterAnnotations: [JavaAnnotation] = getTypeAnnotations(swiftType: swiftType, config: config) + + switch swiftType { + case .nominal(let nominalType): + let nominalTypeName = nominalType.nominalTypeDecl.name + + if let knownType = nominalType.nominalTypeDecl.knownTypeKind { + guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config) else { + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + + guard let translatedClass = javaType.optionalType, let placeholderValue = javaType.optionalPlaceholderValue else { + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + + return TranslatedParameter( + parameter: JavaParameter( + name: parameterName, + type: JavaType(className: translatedClass), + annotations: parameterAnnotations + ), + conversion: .commaSeparated([ + .isOptionalPresent, + .method(.placeholder, function: "orElse", arguments: [.constant(placeholderValue)]) + ]) + ) + } + + if nominalType.isSwiftJavaWrapper { + guard let javaType = nominalTypeName.parseJavaClassFromSwiftJavaName(in: self.javaClassLookupTable) else { + throw JavaTranslationError.wrappedJavaClassTranslationNotProvided(swiftType) + } + + return TranslatedParameter( + parameter: JavaParameter( + name: parameterName, + type: .class(package: nil, name: "Optional<\(javaType)>"), + annotations: parameterAnnotations + ), + conversion: .method( + .placeholder, + function: "orElse", + arguments: [.constant("null")] + ) + ) + } + + // Assume JExtract imported class + return TranslatedParameter( + parameter: JavaParameter( + name: parameterName, + type: .class(package: nil, name: "Optional<\(nominalTypeName)>"), + annotations: parameterAnnotations + ), + conversion: .method( + .method(.placeholder, function: "map", arguments: [.constant("\(nominalType)::$memoryAddress")]), + function: "orElse", + arguments: [.constant("0L")] + ) + ) + default: + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + } + + func translate(swiftResult: SwiftResult, resultName: String = "result") throws -> TranslatedResult { + let swiftType = swiftResult.type + + // If the result type should cause any annotations on the method, include them here. + let resultAnnotations: [JavaAnnotation] = getTypeAnnotations(swiftType: swiftType, config: config) + + switch swiftType { + case .nominal(let nominalType): + if let knownType = nominalType.nominalTypeDecl.knownTypeKind { + switch knownType { + case .optional: + guard let genericArgs = nominalType.genericArguments, genericArgs.count == 1 else { + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + return try translateOptionalResult(wrappedType: genericArgs[0], resultName: resultName) + + case .array: + guard let elementType = nominalType.genericArguments?.first else { + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + return try translateArrayResult( + elementType: elementType + ) + + default: + guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config) else { + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + + return TranslatedResult( + javaType: javaType, + annotations: resultAnnotations, + outParameters: [], + conversion: .placeholder + ) + } + } + + if nominalType.isSwiftJavaWrapper { + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + + // We assume this is a JExtract class. + let javaType = JavaType.class(package: nil, name: nominalType.nominalTypeDecl.name) + return TranslatedResult( + javaType: javaType, + annotations: resultAnnotations, + outParameters: [], + conversion: .wrapMemoryAddressUnsafe(.placeholder, javaType) + ) + + case .tuple([]): + return TranslatedResult(javaType: .void, outParameters: [], conversion: .placeholder) + + case .optional(let wrapped): + return try translateOptionalResult(wrappedType: wrapped, resultName: resultName) + + case .array(let elementType): + return try translateArrayResult( + elementType: elementType + ) + + case .metatype, .tuple, .function, .existential, .opaque, .genericParameter, .composite: + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + } + + func translateOptionalResult( + wrappedType swiftType: SwiftType, + resultName: String = "result" + ) throws -> TranslatedResult { + let discriminatorName = "\(resultName)$_discriminator$" + + let parameterAnnotations: [JavaAnnotation] = getTypeAnnotations(swiftType: swiftType, config: config) + + switch swiftType { + case .nominal(let nominalType): + let nominalTypeName = nominalType.nominalTypeDecl.name + + if let knownType = nominalType.nominalTypeDecl.knownTypeKind { + guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config) else { + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + + guard let returnType = javaType.optionalType, let optionalClass = javaType.optionalWrapperType else { + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + + // Check if we can fit the value and a discriminator byte in a primitive. + // so the return JNI value will be (value, discriminator) + if let nextIntergralTypeWithSpaceForByte = javaType.nextIntergralTypeWithSpaceForByte { + return TranslatedResult( + javaType: .class(package: nil, name: returnType), + annotations: parameterAnnotations, + outParameters: [], + conversion: .combinedValueToOptional( + .placeholder, + nextIntergralTypeWithSpaceForByte.javaType, + resultName: resultName, + valueType: javaType, + valueSizeInBytes: nextIntergralTypeWithSpaceForByte.valueBytes, + optionalType: optionalClass + ) + ) + } else { + // Otherwise, we return the result as normal, but + // use an indirect return for the discriminator. + return TranslatedResult( + javaType: .class(package: nil, name: returnType), + annotations: parameterAnnotations, + outParameters: [ + OutParameter(name: discriminatorName, type: .array(.byte), allocation: .newArray(.byte, size: 1)) + ], + conversion: .toOptionalFromIndirectReturn( + discriminatorName: .combinedName(component: "discriminator$"), + optionalClass: optionalClass, + javaType: javaType, + toValue: .placeholder, + resultName: resultName + ) + ) + } + } + + guard !nominalType.isSwiftJavaWrapper else { + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + + // We assume this is a JExtract class. + let returnType = JavaType.class(package: nil, name: "Optional<\(nominalTypeName)>") + return TranslatedResult( + javaType: returnType, + annotations: parameterAnnotations, + outParameters: [ + OutParameter(name: discriminatorName, type: .array(.byte), allocation: .newArray(.byte, size: 1)) + ], + conversion: .toOptionalFromIndirectReturn( + discriminatorName: .combinedName(component: "discriminator$"), + optionalClass: "Optional", + javaType: .long, + toValue: .wrapMemoryAddressUnsafe(.placeholder, .class(package: nil, name: nominalTypeName)), + resultName: resultName + ) + ) + + default: + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + } + + func translateArrayParameter( + elementType: SwiftType, + parameterName: String + ) throws -> TranslatedParameter { + let parameterAnnotations: [JavaAnnotation] = getTypeAnnotations(swiftType: elementType, config: config) + + switch elementType { + case .nominal(let nominalType): + let nominalTypeName = nominalType.nominalTypeDecl.qualifiedName + + if let knownType = nominalType.nominalTypeDecl.knownTypeKind { + guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config) else { + throw JavaTranslationError.unsupportedSwiftType(elementType) + } + + return TranslatedParameter( + parameter: JavaParameter(name: parameterName, type: .array(javaType), annotations: parameterAnnotations), + conversion: .requireNonNull(.placeholder, message: "\(parameterName) must not be null") + ) + } + + guard !nominalType.isSwiftJavaWrapper else { + throw JavaTranslationError.unsupportedSwiftType(elementType) + } + + // Assume JExtract imported class + return TranslatedParameter( + parameter: JavaParameter( + name: parameterName, + type: .array(.class(package: nil, name: nominalTypeName)), + annotations: parameterAnnotations + ), + conversion: .method( + .method( + .arraysStream(.requireNonNull(.placeholder, message: "\(parameterName) must not be null")), + function: "mapToLong", + arguments: [.constant("\(nominalTypeName)::$memoryAddress")] + ), + function: "toArray", + arguments: [] + ) + ) + + default: + throw JavaTranslationError.unsupportedSwiftType(elementType) + } + } + + func translateArrayResult( + elementType: SwiftType + ) throws -> TranslatedResult { + let annotations: [JavaAnnotation] = getTypeAnnotations(swiftType: elementType, config: config) + + switch elementType { + case .nominal(let nominalType): + let nominalTypeName = nominalType.nominalTypeDecl.qualifiedName + + if let knownType = nominalType.nominalTypeDecl.knownTypeKind { + guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config) else { + throw JavaTranslationError.unsupportedSwiftType(elementType) + } + + return TranslatedResult( + javaType: .array(javaType), + annotations: annotations, + outParameters: [], + conversion: .placeholder + ) + } + + guard !nominalType.isSwiftJavaWrapper else { + throw JavaTranslationError.unsupportedSwiftType(elementType) + } + + let objectType = JavaType.class(package: nil, name: nominalTypeName) + // We assume this is a JExtract class. + return TranslatedResult( + javaType: .array(objectType), + annotations: annotations, + outParameters: [], + conversion: .method( + .method( + .arraysStream(.placeholder), + function: "mapToObj", + arguments: [ + .lambda( + args: ["pointer"], + body: .wrapMemoryAddressUnsafe(.constant("pointer"), objectType) + ) + ] + ), + function: "toArray", + arguments: [.constant("\(objectType)[]::new")] + ) + ) + + default: + throw JavaTranslationError.unsupportedSwiftType(elementType) + } + } + } + + struct TranslatedEnumCase { + /// The corresponding Java case class (CamelCased) + let name: String + + /// The name of the translated enum + let enumName: String + + /// The oringinal enum case. + let original: ImportedEnumCase + + /// A list of the translated associated values + let translatedValues: [TranslatedParameter] + + /// A list of parameter conversions + let parameterConversions: [(translated: TranslatedResult, native: NativeResult)] + + let getAsCaseFunction: TranslatedFunctionDecl + + /// Returns whether the parameters require an arena + var requiresSwiftArena: Bool { + parameterConversions.contains(where: \.translated.conversion.requiresSwiftArena) + } + } + + struct TranslatedFunctionDecl { + /// Java function name + let name: String + + let isStatic: Bool + + let isThrowing: Bool + + let isAsync: Bool + + /// The name of the native function + let nativeFunctionName: String + + /// The name of the Java parent scope this function is declared in + let parentName: String + + /// Functional interfaces required for the Java method. + let functionTypes: [TranslatedFunctionType] + + /// Function signature of the Java function the user will call + let translatedFunctionSignature: TranslatedFunctionSignature + + /// Function signature of the native function that will be implemented by Swift + let nativeFunctionSignature: NativeFunctionSignature + + /// Annotations to include on the Java function declaration + var annotations: [JavaAnnotation] { + self.translatedFunctionSignature.annotations + } + } + + struct TranslatedFunctionSignature { + var selfParameter: TranslatedParameter? + var parameters: [TranslatedParameter] + var resultType: TranslatedResult + + // if the result type implied any annotations, + // propagate them onto the function the result is returned from + var annotations: [JavaAnnotation] { + self.resultType.annotations + } + + var requiresSwiftArena: Bool { + return self.resultType.conversion.requiresSwiftArena + } + } + + /// Represent a Swift API parameter translated to Java. + struct TranslatedParameter { + let parameter: JavaParameter + let conversion: JavaNativeConversionStep + } + + /// Represent a Swift API result translated to Java. + struct TranslatedResult { + let javaType: JavaType + + /// Java annotations that should be propagated from the result type onto the method + var annotations: [JavaAnnotation] = [] + + let outParameters: [OutParameter] + + /// Represents how to convert the Java native result into a user-facing result. + var conversion: JavaNativeConversionStep + } + + struct OutParameter { + enum Allocation { + case newArray(JavaType, size: Int) + case new + + func render(type: JavaType) -> String { + switch self { + case .newArray(let javaType, let size): + "new \(javaType)[\(size)]" + + case .new: + "new \(type)()" + } + } + } + + let name: String + let type: JavaType + let allocation: Allocation + + var javaParameter: JavaParameter { + JavaParameter(name: self.name, type: self.type) + } + } + + /// Represent a Swift closure type in the user facing Java API. + /// + /// Closures are translated to named functional interfaces in Java. + struct TranslatedFunctionType { + var name: String + var parameters: [TranslatedParameter] + var result: TranslatedResult + var swiftType: SwiftFunctionType + } + + /// Describes how to convert values between Java types and the native Java function + enum JavaNativeConversionStep { + /// The value being converted + case placeholder + + case constant(String) + + /// `input_component` + case combinedName(component: String) + + // Convert the results of the inner steps to a comma separated list. + indirect case commaSeparated([JavaNativeConversionStep]) + + /// `value.$memoryAddress()` + indirect case valueMemoryAddress(JavaNativeConversionStep) + + /// `value.$typeMetadataAddress()` + indirect case typeMetadataAddress(JavaNativeConversionStep) + + /// Call `new \(Type)(\(placeholder), swiftArena$)` + indirect case constructSwiftValue(JavaNativeConversionStep, JavaType) + + /// Call `new \(Type)(\(placeholder))` + indirect case constructJavaClass(JavaNativeConversionStep, JavaType) + + /// Call the `MyType.wrapMemoryAddressUnsafe` in order to wrap a memory address using the Java binding type + indirect case wrapMemoryAddressUnsafe(JavaNativeConversionStep, JavaType) + + indirect case call(JavaNativeConversionStep, function: String) + + indirect case method(JavaNativeConversionStep, function: String, arguments: [JavaNativeConversionStep] = []) + + case isOptionalPresent + + indirect case combinedValueToOptional(JavaNativeConversionStep, JavaType, resultName: String, valueType: JavaType, valueSizeInBytes: Int, optionalType: String) + + indirect case ternary(JavaNativeConversionStep, thenExp: JavaNativeConversionStep, elseExp: JavaNativeConversionStep) + + indirect case equals(JavaNativeConversionStep, JavaNativeConversionStep) + + indirect case subscriptOf(JavaNativeConversionStep, arguments: [JavaNativeConversionStep]) + + static func toOptionalFromIndirectReturn( + discriminatorName: JavaNativeConversionStep, + optionalClass: String, + javaType: JavaType, + toValue valueConversion: JavaNativeConversionStep, + resultName: String + ) -> JavaNativeConversionStep { + .aggregate( + variable: (name: "\(resultName)$", type: javaType), + [ + .ternary( + .equals( + .subscriptOf(discriminatorName, arguments: [.constant("0")]), + .constant("1") + ), + thenExp: .method(.constant(optionalClass), function: "of", arguments: [valueConversion]), + elseExp: .method(.constant(optionalClass), function: "empty") + ) + ] + ) + } + + /// Perform multiple conversions using the same input. + case aggregate(variable: (name: String, type: JavaType)? = nil, [JavaNativeConversionStep]) + + indirect case ifStatement(JavaNativeConversionStep, thenExp: JavaNativeConversionStep, elseExp: JavaNativeConversionStep? = nil) + + /// Access a member of the value + indirect case replacingPlaceholder(JavaNativeConversionStep, placeholder: String) + + /// `(args) -> { return body; }` + indirect case lambda(args: [String] = [], body: JavaNativeConversionStep) + + /// Prints the conversion step, ignoring the output. + indirect case print(JavaNativeConversionStep) + + indirect case requireNonNull(JavaNativeConversionStep, message: String) + + /// `Arrays.stream(args)` + static func arraysStream(_ argument: JavaNativeConversionStep) -> JavaNativeConversionStep { + .method(.constant("Arrays"), function: "stream", arguments: [argument]) + } + + /// Returns the conversion string applied to the placeholder. + func render(_ printer: inout CodePrinter, _ placeholder: String) -> String { + // NOTE: 'printer' is used if the conversion wants to cause side-effects. + // E.g. storing a temporary values into a variable. + switch self { + case .placeholder: + return placeholder + + case .constant(let value): + return value + + case .combinedName(let component): + return "\(placeholder)_\(component)" + + case .commaSeparated(let list): + return list.map({ $0.render(&printer, placeholder)}).joined(separator: ", ") + + case .valueMemoryAddress: + return "\(placeholder).$memoryAddress()" + + case .typeMetadataAddress(let inner): + let inner = inner.render(&printer, placeholder) + return "\(inner).$typeMetadataAddress()" + + case .constructSwiftValue(let inner, let javaType): + let inner = inner.render(&printer, placeholder) + return "new \(javaType.className!)(\(inner), swiftArena$)" + + case .wrapMemoryAddressUnsafe(let inner, let javaType): + let inner = inner.render(&printer, placeholder) + return "\(javaType.className!).wrapMemoryAddressUnsafe(\(inner), swiftArena$)" + + case .constructJavaClass(let inner, let javaType): + let inner = inner.render(&printer, placeholder) + return "new \(javaType.className!)(\(inner))" + + case .call(let inner, let function): + let inner = inner.render(&printer, placeholder) + return "\(function)(\(inner))" + + + case .isOptionalPresent: + return "(byte) (\(placeholder).isPresent() ? 1 : 0)" + + case .method(let inner, let methodName, let arguments): + let inner = inner.render(&printer, placeholder) + let args = arguments.map { $0.render(&printer, placeholder) } + let argsStr = args.joined(separator: ", ") + return "\(inner).\(methodName)(\(argsStr))" + + case .combinedValueToOptional(let combined, let combinedType, let resultName, let valueType, let valueSizeInBytes, let optionalType): + let combined = combined.render(&printer, placeholder) + printer.print( + """ + \(combinedType) \(resultName)_combined$ = \(combined); + byte \(resultName)_discriminator$ = (byte) (\(resultName)_combined$ & 0xFF); + """ + ) + + if valueType == .boolean { + printer.print("boolean \(resultName)_value$ = ((byte) (\(resultName)_combined$ >> 8)) != 0;") + } else { + printer.print("\(valueType) \(resultName)_value$ = (\(valueType)) (\(resultName)_combined$ >> \(valueSizeInBytes * 8));") + } + + return "\(resultName)_discriminator$ == 1 ? \(optionalType).of(\(resultName)_value$) : \(optionalType).empty()" + + case .ternary(let cond, let thenExp, let elseExp): + let cond = cond.render(&printer, placeholder) + let thenExp = thenExp.render(&printer, placeholder) + let elseExp = elseExp.render(&printer, placeholder) + return "(\(cond)) ? \(thenExp) : \(elseExp)" + + case .equals(let lhs, let rhs): + let lhs = lhs.render(&printer, placeholder) + let rhs = rhs.render(&printer, placeholder) + return "\(lhs) == \(rhs)" + + case .subscriptOf(let inner, let arguments): + let inner = inner.render(&printer, placeholder) + let arguments = arguments.map { $0.render(&printer, placeholder) } + return "\(inner)[\(arguments.joined(separator: ", "))]" + + case .aggregate(let variable, let steps): + precondition(!steps.isEmpty, "Aggregate must contain steps") + let toExplode: String + if let variable { + printer.print("\(variable.type) \(variable.name) = \(placeholder);") + toExplode = variable.name + } else { + toExplode = placeholder + } + let steps = steps.map { + $0.render(&printer, toExplode) + } + return steps.last! + + case .ifStatement(let cond, let thenExp, let elseExp): + let cond = cond.render(&printer, placeholder) + printer.printBraceBlock("if (\(cond))") { printer in + printer.print(thenExp.render(&printer, placeholder)) + } + if let elseExp { + printer.printBraceBlock("else") { printer in + printer.print(elseExp.render(&printer, placeholder)) + } + } + + return "" + + case .replacingPlaceholder(let inner, let placeholder): + return inner.render(&printer, placeholder) + + case .lambda(let args, let body): + var printer = CodePrinter() + printer.printBraceBlock("(\(args.joined(separator: ", "))) ->") { printer in + let body = body.render(&printer, placeholder) + if !body.isEmpty { + printer.print("return \(body);") + } else { + printer.print("return;") + } + } + return printer.finalize() + + case .print(let inner): + let inner = inner.render(&printer, placeholder) + printer.print("\(inner);") + return "" + + case .requireNonNull(let inner, let message): + let inner = inner.render(&printer, placeholder) + return #"Objects.requireNonNull(\#(inner), "\#(message)")"# + } + } + + /// Whether the conversion uses SwiftArena. + var requiresSwiftArena: Bool { + switch self { + case .placeholder, .constant, .isOptionalPresent, .combinedName: + return false + + case .constructSwiftValue, .wrapMemoryAddressUnsafe: + return true + + case .constructJavaClass(let inner, _): + return inner.requiresSwiftArena + + case .valueMemoryAddress(let inner): + return inner.requiresSwiftArena + + case .typeMetadataAddress(let inner): + return inner.requiresSwiftArena + + case .commaSeparated(let list): + return list.contains(where: { $0.requiresSwiftArena }) + + case .method(let inner, _, let args): + return inner.requiresSwiftArena || args.contains(where: \.requiresSwiftArena) + + case .combinedValueToOptional(let inner, _, _, _, _, _): + return inner.requiresSwiftArena + + case .ternary(let cond, let thenExp, let elseExp): + return cond.requiresSwiftArena || thenExp.requiresSwiftArena || elseExp.requiresSwiftArena + + case .equals(let lhs, let rhs): + return lhs.requiresSwiftArena || rhs.requiresSwiftArena + + case .subscriptOf(let inner, _): + return inner.requiresSwiftArena + + case .aggregate(_, let steps): + return steps.contains(where: \.requiresSwiftArena) + + case .ifStatement(let cond, let thenExp, let elseExp): + return cond.requiresSwiftArena || thenExp.requiresSwiftArena || (elseExp?.requiresSwiftArena ?? false) + + case .call(let inner, _): + return inner.requiresSwiftArena + + case .replacingPlaceholder(let inner, _): + return inner.requiresSwiftArena + + case .lambda(_, let body): + return body.requiresSwiftArena + + case .print(let inner): + return inner.requiresSwiftArena + + case .requireNonNull(let inner, _): + return inner.requiresSwiftArena + } + } + } + + enum JavaTranslationError: Error { + case unsupportedSwiftType(SwiftType, fileID: String, line: Int) + static func unsupportedSwiftType(_ type: SwiftType, _fileID: String = #fileID, _line: Int = #line) -> JavaTranslationError { + .unsupportedSwiftType(type, fileID: _fileID, line: _line) + } + + /// The user has not supplied a mapping from `SwiftType` to + /// a java class. + case wrappedJavaClassTranslationNotProvided(SwiftType) + + // FIXME: Remove once we support protocol variables + case protocolVariablesNotSupported + } +} diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift new file mode 100644 index 000000000..b744a33b4 --- /dev/null +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -0,0 +1,1114 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JavaTypes +import SwiftJavaConfigurationShared + +extension JNISwift2JavaGenerator { + + struct NativeJavaTranslation { + let config: Configuration + let javaPackage: String + let javaClassLookupTable: JavaClassLookupTable + var knownTypes: SwiftKnownTypes + let protocolWrappers: [ImportedNominalType: JavaInterfaceSwiftWrapper] + + /// Translates a Swift function into the native JNI method signature. + func translate( + functionSignature: SwiftFunctionSignature, + translatedFunctionSignature: TranslatedFunctionSignature, + methodName: String, + parentName: String + ) throws -> NativeFunctionSignature { + let parameters = try zip(translatedFunctionSignature.parameters, functionSignature.parameters).map { + translatedParameter, + swiftParameter in + let parameterName = translatedParameter.parameter.name + return try translateParameter( + type: swiftParameter.type, + parameterName: parameterName, + methodName: methodName, + parentName: parentName, + genericParameters: functionSignature.genericParameters, + genericRequirements: functionSignature.genericRequirements + ) + } + + // Lower the self parameter. + let nativeSelf: NativeParameter? = switch functionSignature.selfParameter { + case .instance(let selfParameter): + try translateParameter( + type: selfParameter.type, + parameterName: selfParameter.parameterName ?? "self", + methodName: methodName, + parentName: parentName, + genericParameters: functionSignature.genericParameters, + genericRequirements: functionSignature.genericRequirements + ) + case nil, .initializer(_), .staticMethod(_): + nil + } + + let result = try translate(swiftResult: functionSignature.result) + + return NativeFunctionSignature( + selfParameter: nativeSelf, + parameters: parameters, + result: result + ) + } + + func translateParameter( + type: SwiftType, + parameterName: String, + methodName: String, + parentName: String, + genericParameters: [SwiftGenericParameterDeclaration], + genericRequirements: [SwiftGenericRequirement] + ) throws -> NativeParameter { + switch type { + case .nominal(let nominalType): + let nominalTypeName = nominalType.nominalTypeDecl.name + + if let knownType = nominalType.nominalTypeDecl.knownTypeKind { + switch knownType { + case .optional: + guard let genericArgs = nominalType.genericArguments, genericArgs.count == 1 else { + throw JavaTranslationError.unsupportedSwiftType(type) + } + return try translateOptionalParameter( + wrappedType: genericArgs[0], + parameterName: parameterName + ) + + case .array: + guard let elementType = nominalType.genericArguments?.first else { + throw JavaTranslationError.unsupportedSwiftType(type) + } + return try translateArrayParameter(elementType: elementType, parameterName: parameterName) + + default: + guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config), + javaType.implementsJavaValue else { + throw JavaTranslationError.unsupportedSwiftType(type) + } + + return NativeParameter( + parameters: [ + JavaParameter(name: parameterName, type: javaType) + ], + conversion: .initFromJNI(.placeholder, swiftType: type) + ) + + } + } + + if nominalType.isSwiftJavaWrapper { + guard let javaType = nominalTypeName.parseJavaClassFromSwiftJavaName(in: self.javaClassLookupTable) else { + throw JavaTranslationError.wrappedJavaClassTranslationNotProvided(type) + } + + return NativeParameter( + parameters: [ + JavaParameter(name: parameterName, type: javaType) + ], + conversion: .initializeSwiftJavaWrapper( + .unwrapOptional( + .placeholder, + name: parameterName, + fatalErrorMessage: "\(parameterName) was null in call to \\(#function), but Swift requires non-optional!" + ), + wrapperName: nominalTypeName + ) + ) + } + + // JExtract classes are passed as the pointer. + return NativeParameter( + parameters: [ + JavaParameter(name: parameterName, type: .long) + ], + conversion: .pointee(.extractSwiftValue(.placeholder, swiftType: type)) + ) + + case .tuple([]): + return NativeParameter( + parameters: [ + JavaParameter(name: parameterName, type: .void) + ], + conversion: .placeholder + ) + + case .function(let fn): + var parameters = [NativeParameter]() + for (i, parameter) in fn.parameters.enumerated() { + let parameterName = parameter.parameterName ?? "_\(i)" + let closureParameter = try translateClosureParameter( + parameter.type, + parameterName: parameterName + ) + parameters.append(closureParameter) + } + + let result = try translateClosureResult(fn.resultType) + + return NativeParameter( + parameters: [ + JavaParameter(name: parameterName, type: .class(package: javaPackage, name: "\(parentName).\(methodName).\(parameterName)")) + ], + conversion: .closureLowering( + parameters: parameters, + result: result + ) + ) + + case .optional(let wrapped): + return try translateOptionalParameter( + wrappedType: wrapped, + parameterName: parameterName + ) + + case .opaque(let proto), .existential(let proto): + return try translateProtocolParameter( + protocolType: proto, + methodName: methodName, + parameterName: parameterName, + parentName: parentName + ) + + case .genericParameter: + if let concreteTy = type.typeIn(genericParameters: genericParameters, genericRequirements: genericRequirements) { + return try translateProtocolParameter( + protocolType: concreteTy, + methodName: methodName, + parameterName: parameterName, + parentName: parentName + ) + } + + throw JavaTranslationError.unsupportedSwiftType(type) + + case .array(let elementType): + return try translateArrayParameter( + elementType: elementType, + parameterName: parameterName + ) + + case .metatype, .tuple, .composite: + throw JavaTranslationError.unsupportedSwiftType(type) + } + } + + func translateProtocolParameter( + protocolType: SwiftType, + methodName: String, + parameterName: String, + parentName: String? + ) throws -> NativeParameter { + switch protocolType { + case .nominal(let nominalType): + return try translateProtocolParameter(protocolTypes: [nominalType], methodName: methodName, parameterName: parameterName, parentName: parentName) + + case .composite(let types): + let protocolTypes = try types.map { + guard let nominalTypeName = $0.asNominalType else { + throw JavaTranslationError.unsupportedSwiftType($0) + } + return nominalTypeName + } + + return try translateProtocolParameter(protocolTypes: protocolTypes, methodName: methodName, parameterName: parameterName, parentName: parentName) + + default: + throw JavaTranslationError.unsupportedSwiftType(protocolType) + } + } + + private func translateProtocolParameter( + protocolTypes: [SwiftNominalType], + methodName: String, + parameterName: String, + parentName: String? + ) throws -> NativeParameter { + // We allow Java implementations if we are able to generate the needed + // Swift wrappers for all the protocol types. + let allowsJavaImplementations = protocolTypes.allSatisfy { protocolType in + self.protocolWrappers.contains(where: { $0.value.protocolType == protocolType }) + } + + return NativeParameter( + parameters: [ + JavaParameter(name: parameterName, type: .javaLangObject) + ], + conversion: .interfaceToSwiftObject( + .placeholder, + swiftWrapperClassName: JNISwift2JavaGenerator.protocolParameterWrapperClassName( + methodName: methodName, + parameterName: parameterName, + parentName: parentName + ), + protocolTypes: protocolTypes, + allowsJavaImplementations: allowsJavaImplementations + ) + ) + } + + func translateOptionalParameter( + wrappedType swiftType: SwiftType, + parameterName: String + ) throws -> NativeParameter { + let discriminatorName = "\(parameterName)_discriminator" + let valueName = "\(parameterName)_value" + + switch swiftType { + case .nominal(let nominalType): + let nominalTypeName = nominalType.nominalTypeDecl.name + + if let knownType = nominalType.nominalTypeDecl.knownTypeKind { + guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config), + javaType.implementsJavaValue else { + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + + return NativeParameter( + parameters: [ + JavaParameter(name: discriminatorName, type: .byte), + JavaParameter(name: valueName, type: javaType) + ], + conversion: .optionalLowering( + .initFromJNI(.placeholder, swiftType: swiftType), + discriminatorName: discriminatorName, + valueName: valueName + ) + ) + } + + if nominalType.isSwiftJavaWrapper { + guard let javaType = nominalTypeName.parseJavaClassFromSwiftJavaName(in: self.javaClassLookupTable) else { + throw JavaTranslationError.wrappedJavaClassTranslationNotProvided(swiftType) + } + + return NativeParameter( + parameters: [ + JavaParameter(name: parameterName, type: javaType) + ], + conversion: .optionalMap(.initializeSwiftJavaWrapper(.placeholder, wrapperName: nominalTypeName)) + ) + } + + // Assume JExtract wrapped class + return NativeParameter( + parameters: [JavaParameter(name: parameterName, type: .long)], + conversion: .pointee( + .optionalChain( + .extractSwiftValue( + .placeholder, + swiftType: swiftType, + allowNil: true + ) + ) + ) + ) + + default: + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + } + + func translateOptionalResult( + wrappedType swiftType: SwiftType, + resultName: String = "result" + ) throws -> NativeResult { + let discriminatorName = "\(resultName)_discriminator$" + + switch swiftType { + case .nominal(let nominalType): + if let knownType = nominalType.nominalTypeDecl.knownTypeKind { + guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config), + javaType.implementsJavaValue else { + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + + // Check if we can fit the value and a discriminator byte in a primitive. + // so the return JNI value will be (value, discriminator) + if let nextIntergralTypeWithSpaceForByte = javaType.nextIntergralTypeWithSpaceForByte { + return NativeResult( + javaType: nextIntergralTypeWithSpaceForByte.javaType, + conversion: .getJNIValue( + .optionalRaisingWidenIntegerType( + .placeholder, + resultName: resultName, + valueType: javaType, + combinedSwiftType: nextIntergralTypeWithSpaceForByte.swiftType, + valueSizeInBytes: nextIntergralTypeWithSpaceForByte.valueBytes + ) + ), + outParameters: [] + ) + } else { + // Use indirect byte array to store discriminator + + return NativeResult( + javaType: javaType, + conversion: .optionalRaisingIndirectReturn( + .getJNIValue(.placeholder), + returnType: javaType, + discriminatorParameterName: discriminatorName, + placeholderValue: .member( + .constant("\(swiftType)"), + member: "jniPlaceholderValue" + ) + ), + outParameters: [ + JavaParameter(name: discriminatorName, type: .array(.byte)) + ] + ) + } + } + + guard !nominalType.isSwiftJavaWrapper else { + // TODO: Should be the same as above + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + + // Assume JExtract imported class + return NativeResult( + javaType: .long, + conversion: .optionalRaisingIndirectReturn( + .getJNIValue(.allocateSwiftValue(.placeholder, name: "_result", swiftType: swiftType)), + returnType: .long, + discriminatorParameterName: discriminatorName, + placeholderValue: .constant("0") + ), + outParameters: [ + JavaParameter(name: discriminatorName, type: .array(.byte)) + ] + ) + + default: + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + } + + func translateClosureResult( + _ type: SwiftType + ) throws -> NativeResult { + switch type { + case .nominal(let nominal): + if let knownType = nominal.nominalTypeDecl.knownTypeKind { + guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config), + javaType.implementsJavaValue else { + throw JavaTranslationError.unsupportedSwiftType(type) + } + + // Only support primitives for now. + return NativeResult( + javaType: javaType, + conversion: .initFromJNI(.placeholder, swiftType: type), + outParameters: [] + ) + } + + // Custom types are not supported yet. + throw JavaTranslationError.unsupportedSwiftType(type) + + case .tuple([]): + return NativeResult( + javaType: .void, + conversion: .placeholder, + outParameters: [] + ) + + case .function, .metatype, .optional, .tuple, .existential, .opaque, .genericParameter, .composite, .array: + throw JavaTranslationError.unsupportedSwiftType(type) + } + } + + func translateClosureParameter( + _ type: SwiftType, + parameterName: String + ) throws -> NativeParameter { + switch type { + case .nominal(let nominal): + if let knownType = nominal.nominalTypeDecl.knownTypeKind { + guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config), + javaType.implementsJavaValue else { + throw JavaTranslationError.unsupportedSwiftType(type) + } + + // Only support primitives for now. + return NativeParameter( + parameters: [ + JavaParameter(name: parameterName, type: javaType) + ], + conversion: .getJValue(.placeholder) + ) + } + + // Custom types are not supported yet. + throw JavaTranslationError.unsupportedSwiftType(type) + + case .function, .metatype, .optional, .tuple, .existential, .opaque, .genericParameter, .composite, .array: + throw JavaTranslationError.unsupportedSwiftType(type) + } + } + + func translate( + swiftResult: SwiftResult, + resultName: String = "result" + ) throws -> NativeResult { + switch swiftResult.type { + case .nominal(let nominalType): + if let knownType = nominalType.nominalTypeDecl.knownTypeKind { + switch knownType { + case .optional: + guard let genericArgs = nominalType.genericArguments, genericArgs.count == 1 else { + throw JavaTranslationError.unsupportedSwiftType(swiftResult.type) + } + return try translateOptionalResult(wrappedType: genericArgs[0], resultName: resultName) + + case .array: + guard let elementType = nominalType.genericArguments?.first else { + throw JavaTranslationError.unsupportedSwiftType(swiftResult.type) + } + return try translateArrayResult(elementType: elementType, resultName: resultName) + + default: + guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config), javaType.implementsJavaValue else { + throw JavaTranslationError.unsupportedSwiftType(swiftResult.type) + } + + return NativeResult( + javaType: javaType, + conversion: .getJNIValue(.placeholder), + outParameters: [] + ) + } + } + + if nominalType.isSwiftJavaWrapper { + throw JavaTranslationError.unsupportedSwiftType(swiftResult.type) + } + + return NativeResult( + javaType: .long, + conversion: .getJNIValue(.allocateSwiftValue(.placeholder, name: resultName, swiftType: swiftResult.type)), + outParameters: [] + ) + + case .tuple([]): + return NativeResult( + javaType: .void, + conversion: .placeholder, + outParameters: [] + ) + + case .optional(let wrapped): + return try translateOptionalResult(wrappedType: wrapped, resultName: resultName) + + case .array(let elementType): + return try translateArrayResult(elementType: elementType, resultName: resultName) + + case .metatype, .tuple, .function, .existential, .opaque, .genericParameter, .composite: + throw JavaTranslationError.unsupportedSwiftType(swiftResult.type) + } + } + + func translateArrayResult( + elementType: SwiftType, + resultName: String + ) throws -> NativeResult { + switch elementType { + case .nominal(let nominalType): + if let knownType = nominalType.nominalTypeDecl.knownTypeKind { + guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config), + javaType.implementsJavaValue else { + throw JavaTranslationError.unsupportedSwiftType(.array(elementType)) + } + + return NativeResult( + javaType: .array(javaType), + conversion: .getJNIValue(.placeholder), + outParameters: [] + ) + } + + guard !nominalType.isSwiftJavaWrapper else { + throw JavaTranslationError.unsupportedSwiftType(.array(elementType)) + } + + // Assume JExtract imported class + return NativeResult( + javaType: .array(.long), + conversion: + .getJNIValue( + .method( + .placeholder, + function: "map", + arguments: [ + (nil, .closure( + args: ["object$"], + body: .allocateSwiftValue(.constant("object$"), name: "object$", swiftType: elementType) + )) + ] + ) + ), + outParameters: [] + ) + + default: + throw JavaTranslationError.unsupportedSwiftType(.array(elementType)) + } + } + + func translateArrayParameter( + elementType: SwiftType, + parameterName: String + ) throws -> NativeParameter { + switch elementType { + case .nominal(let nominalType): + if let knownType = nominalType.nominalTypeDecl.knownTypeKind { + guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config), + javaType.implementsJavaValue else { + throw JavaTranslationError.unsupportedSwiftType(elementType) + } + + return NativeParameter( + parameters: [ + JavaParameter(name: parameterName, type: .array(javaType)), + ], + conversion: .initFromJNI(.placeholder, swiftType: .array(elementType)) + ) + } + + guard !nominalType.isSwiftJavaWrapper else { + throw JavaTranslationError.unsupportedSwiftType(.array(elementType)) + } + + // Assume JExtract wrapped class + return NativeParameter( + parameters: [JavaParameter(name: parameterName, type: .array(.long))], + conversion: .method( + .initFromJNI(.placeholder, swiftType: .array(self.knownTypes.int64)), + function: "map", + arguments: [ + (nil, .closure( + args: ["pointer$"], + body: .pointee(.extractSwiftValue( + .constant("pointer$"), + swiftType: elementType, + allowNil: false, + convertLongFromJNI: false + )))) + ] + ) + ) + + default: + throw JavaTranslationError.unsupportedSwiftType(elementType) + } + } + } + + struct NativeFunctionSignature { + let selfParameter: NativeParameter? + var parameters: [NativeParameter] + var result: NativeResult + } + + struct NativeParameter { + /// One Swift parameter can be lowered to multiple parameters. + /// E.g. 'Optional' as (descriptor, value) pair. + var parameters: [JavaParameter] + + /// Represents how to convert the JNI parameter to a Swift parameter + let conversion: NativeSwiftConversionStep + } + + struct NativeResult { + var javaType: JavaType + var conversion: NativeSwiftConversionStep + + /// Out parameters for populating the indirect return values. + var outParameters: [JavaParameter] + } + + /// Describes how to convert values between Java types and Swift through JNI + enum NativeSwiftConversionStep { + /// The value being converted + case placeholder + + case constant(String) + + /// `input_component` + case combinedName(component: String) + + /// `value.getJNIValue(in:)` + indirect case getJNIValue(NativeSwiftConversionStep) + + /// `value.getJValue(in:)` + indirect case getJValue(NativeSwiftConversionStep) + + /// `SwiftType(from: value, in: environment)` + indirect case initFromJNI(NativeSwiftConversionStep, swiftType: SwiftType) + + indirect case interfaceToSwiftObject( + NativeSwiftConversionStep, + swiftWrapperClassName: String, + protocolTypes: [SwiftNominalType], + allowsJavaImplementations: Bool + ) + + indirect case extractSwiftProtocolValue( + NativeSwiftConversionStep, + typeMetadataVariableName: NativeSwiftConversionStep, + protocolNames: [String] + ) + + /// Extracts a swift type at a pointer given by a long. + indirect case extractSwiftValue( + NativeSwiftConversionStep, + swiftType: SwiftType, + allowNil: Bool = false, + convertLongFromJNI: Bool = true + ) + + /// Allocate memory for a Swift value and outputs the pointer + indirect case allocateSwiftValue(NativeSwiftConversionStep, name: String, swiftType: SwiftType) + + /// The thing to which the pointer typed, which is the `pointee` property + /// of the `Unsafe(Mutable)Pointer` types in Swift. + indirect case pointee(NativeSwiftConversionStep) + + indirect case closureLowering(parameters: [NativeParameter], result: NativeResult) + + indirect case initializeSwiftJavaWrapper(NativeSwiftConversionStep, wrapperName: String) + + indirect case optionalLowering(NativeSwiftConversionStep, discriminatorName: String, valueName: String) + + indirect case optionalChain(NativeSwiftConversionStep) + + indirect case optionalRaisingWidenIntegerType(NativeSwiftConversionStep, resultName: String, valueType: JavaType, combinedSwiftType: SwiftKnownTypeDeclKind, valueSizeInBytes: Int) + + indirect case optionalRaisingIndirectReturn(NativeSwiftConversionStep, returnType: JavaType, discriminatorParameterName: String, placeholderValue: NativeSwiftConversionStep) + + indirect case method(NativeSwiftConversionStep, function: String, arguments: [(String?, NativeSwiftConversionStep)] = []) + + indirect case member(NativeSwiftConversionStep, member: String) + + indirect case optionalMap(NativeSwiftConversionStep) + + indirect case unwrapOptional(NativeSwiftConversionStep, name: String, fatalErrorMessage: String) + + indirect case asyncCompleteFuture( + swiftFunctionResultType: SwiftType, + nativeFunctionSignature: NativeFunctionSignature, + isThrowing: Bool, + completeMethodID: String, + completeExceptionallyMethodID: String + ) + + /// `{ (args) -> return body }` + indirect case closure(args: [String] = [], body: NativeSwiftConversionStep) + + /// Returns the conversion string applied to the placeholder. + func render(_ printer: inout CodePrinter, _ placeholder: String) -> String { + // NOTE: 'printer' is used if the conversion wants to cause side-effects. + // E.g. storing a temporary values into a variable. + switch self { + case .placeholder: + return placeholder + + case .constant(let value): + return value + + case .combinedName(let component): + return "\(placeholder)_\(component)" + + case .getJNIValue(let inner): + let inner = inner.render(&printer, placeholder) + return "\(inner).getJNIValue(in: environment)" + + case .getJValue(let inner): + let inner = inner.render(&printer, placeholder) + return "\(inner).getJValue(in: environment)" + + case .initFromJNI(let inner, let swiftType): + let inner = inner.render(&printer, placeholder) + return "\(swiftType)(fromJNI: \(inner), in: environment)" + + case .interfaceToSwiftObject( + let inner, + let swiftWrapperClassName, + let protocolTypes, + let allowsJavaImplementations + ): + let protocolNames = protocolTypes.map { $0.nominalTypeDecl.qualifiedName } + + let inner = inner.render(&printer, placeholder) + let variableName = "\(inner)swiftObject$" + let compositeProtocolName = "(\(protocolNames.joined(separator: " & ")))" + printer.print("let \(variableName): \(compositeProtocolName)") + + func printStandardJExtractBlock(_ printer: inout CodePrinter) { + let pointerVariableName = "\(inner)pointer$" + let typeMetadataVariableName = "\(inner)typeMetadata$" + printer.print( + """ + let \(pointerVariableName) = environment.interface.CallLongMethodA(environment, \(inner), _JNIMethodIDCache.JNISwiftInstance.memoryAddress, []) + let \(typeMetadataVariableName) = environment.interface.CallLongMethodA(environment, \(inner), _JNIMethodIDCache.JNISwiftInstance.typeMetadataAddress, []) + """ + ) + let existentialName = NativeSwiftConversionStep.extractSwiftProtocolValue( + .constant(pointerVariableName), + typeMetadataVariableName: .constant(typeMetadataVariableName), + protocolNames: protocolNames + ).render(&printer, placeholder) + + printer.print("\(variableName) = \(existentialName)") + } + + // If this protocol type supports being implemented by the user + // then we will check whether it is a JNI SwiftInstance type + // or if its a custom class implementing the interface. + if allowsJavaImplementations { + printer.printBraceBlock( + "if environment.interface.IsInstanceOf(environment, \(inner), _JNIMethodIDCache.JNISwiftInstance.class) != 0" + ) { printer in + printStandardJExtractBlock(&printer) + } + printer.printBraceBlock("else") { printer in + let arguments = protocolTypes.map { protocolType in + let nominalTypeDecl = protocolType.nominalTypeDecl + return "\(nominalTypeDecl.javaInterfaceVariableName): \(nominalTypeDecl.javaInterfaceName)(javaThis: \(inner)!, environment: environment)" + } + printer.print("\(variableName) = \(swiftWrapperClassName)(\(arguments.joined(separator: ", ")))") + } + } else { + printStandardJExtractBlock(&printer) + } + + + return variableName + + case .extractSwiftProtocolValue(let inner, let typeMetadataVariableName, let protocolNames): + let inner = inner.render(&printer, placeholder) + let typeMetadataVariableName = typeMetadataVariableName.render(&printer, placeholder) + let existentialName = "\(inner)Existential$" + + let compositeProtocolName = "(\(protocolNames.joined(separator: " & ")))" + + // TODO: Remove the _openExistential when we decide to only support language mode v6+ + printer.print( + """ + guard let \(inner)TypeMetadataPointer$ = UnsafeRawPointer(bitPattern: Int(Int64(fromJNI: \(typeMetadataVariableName), in: environment))) else { + fatalError("\(typeMetadataVariableName) memory address was null") + } + let \(inner)DynamicType$: Any.Type = unsafeBitCast(\(inner)TypeMetadataPointer$, to: Any.Type.self) + guard let \(inner)RawPointer$ = UnsafeMutableRawPointer(bitPattern: Int(Int64(fromJNI: \(inner), in: environment))) else { + fatalError("\(inner) memory address was null") + } + #if hasFeature(ImplicitOpenExistentials) + let \(existentialName) = \(inner)RawPointer$.load(as: \(inner)DynamicType$) as! any \(compositeProtocolName) + #else + func \(inner)DoLoad(_ ty: Ty.Type) -> any \(compositeProtocolName) { + \(inner)RawPointer$.load(as: ty) as! any \(compositeProtocolName) + } + let \(existentialName) = _openExistential(\(inner)DynamicType$, do: \(inner)DoLoad) + #endif + """ + ) + return existentialName + + case .extractSwiftValue(let inner, let swiftType, let allowNil, let convertLongFromJNI): + let inner = inner.render(&printer, placeholder) + let pointerName = "\(inner)$" + if !allowNil { + printer.print(#"assert(\#(inner) != 0, "\#(inner) memory address was null")"#) + } + if convertLongFromJNI { + printer.print("let \(inner)Bits$ = Int(Int64(fromJNI: \(inner), in: environment))") + } else { + printer.print("let \(inner)Bits$ = Int(\(inner))") + } + printer.print("let \(pointerName) = UnsafeMutablePointer<\(swiftType)>(bitPattern: \(inner)Bits$)") + if !allowNil { + printer.print( + """ + guard let \(pointerName) else { + fatalError("\(inner) memory address was null in call to \\(#function)!") + } + """ + ) + } + return pointerName + + case .allocateSwiftValue(let inner, let name, let swiftType): + let inner = inner.render(&printer, placeholder) + let pointerName = "\(name)$" + let bitsName = "\(name)Bits$" + printer.print( + """ + let \(pointerName) = UnsafeMutablePointer<\(swiftType)>.allocate(capacity: 1) + \(pointerName).initialize(to: \(inner)) + let \(bitsName) = Int64(Int(bitPattern: \(pointerName))) + """ + ) + return bitsName + + case .pointee(let inner): + let inner = inner.render(&printer, placeholder) + return "\(inner).pointee" + + case .closureLowering(let parameters, let nativeResult): + var printer = CodePrinter() + + let methodSignature = MethodSignature( + resultType: nativeResult.javaType, + parameterTypes: parameters.flatMap { + $0.parameters.map { parameter in + guard case .concrete(let type) = parameter.type else { + fatalError("Closures do not support Java generics") + } + return type + } + } + ) + + let names = parameters.flatMap { $0.parameters.map(\.name) } + let closureParameters = !parameters.isEmpty ? "\(names.joined(separator: ", ")) in" : "" + printer.print("{ \(closureParameters)") + printer.indent() + + // TODO: Add support for types that are lowered to multiple parameters in closures + let arguments = parameters.map { + $0.conversion.render(&printer, $0.parameters.first!.name) + } + + printer.print( + """ + let class$ = environment.interface.GetObjectClass(environment, \(placeholder)) + let methodID$ = environment.interface.GetMethodID(environment, class$, "apply", "\(methodSignature.mangledName)")! + let arguments$: [jvalue] = [\(arguments.joined(separator: ", "))] + """ + ) + + let upcall = "environment.interface.\(nativeResult.javaType.jniCallMethodAName)(environment, \(placeholder), methodID$, arguments$)" + let result = nativeResult.conversion.render(&printer, upcall) + + if nativeResult.javaType.isVoid { + printer.print(result) + } else { + printer.print("return \(result)") + } + + printer.outdent() + printer.print("}") + + return printer.finalize() + + case .initializeSwiftJavaWrapper(let inner, let wrapperName): + let inner = inner.render(&printer, placeholder) + return "\(wrapperName)(javaThis: \(inner), environment: environment)" + + case .optionalLowering(let valueConversion, let discriminatorName, let valueName): + let value = valueConversion.render(&printer, valueName) + return "\(discriminatorName) == 1 ? \(value) : nil" + + case .optionalChain(let inner): + let inner = inner.render(&printer, placeholder) + return "\(inner)?" + + case .optionalRaisingWidenIntegerType(let inner, let resultName, let valueType, let combinedSwiftType, let valueSizeInBytes): + let inner = inner.render(&printer, placeholder) + let value = valueType == .boolean ? "$0 ? 1 : 0" : "$0" + let combinedSwiftTypeName = combinedSwiftType.moduleAndName.name + printer.print( + """ + let \(resultName)_value$ = \(inner).map { + \(combinedSwiftTypeName)(\(value)) << \(valueSizeInBytes * 8) | \(combinedSwiftTypeName)(1) + } ?? 0 + """ + ) + return "\(resultName)_value$" + + case .optionalRaisingIndirectReturn(let inner, let returnType, let discriminatorParameterName, let placeholderValue): + printer.print("let result$: \(returnType.jniTypeName)") + printer.printBraceBlock("if let innerResult$ = \(placeholder)") { printer in + let inner = inner.render(&printer, "innerResult$") + printer.print( + """ + result$ = \(inner) + var flag$ = Int8(1) + environment.interface.SetByteArrayRegion(environment, \(discriminatorParameterName), 0, 1, &flag$) + """ + ) + } + printer.printBraceBlock("else") { printer in + let placeholderValue = placeholderValue.render(&printer, placeholder) + printer.print( + """ + result$ = \(placeholderValue) + var flag$ = Int8(0) + environment.interface.SetByteArrayRegion(environment, \(discriminatorParameterName), 0, 1, &flag$) + """ + ) + } + + return "result$" + + case .method(let inner, let methodName, let arguments): + let inner = inner.render(&printer, placeholder) + let args = arguments.map { name, value in + let value = value.render(&printer, placeholder) + if let name { + return "\(name): \(value)" + } else { + return value + } + } + let argsStr = args.joined(separator: ", ") + return "\(inner).\(methodName)(\(argsStr))" + + case .member(let inner, let member): + let inner = inner.render(&printer, placeholder) + return "\(inner).\(member)" + + case .optionalMap(let inner): + var printer = CodePrinter() + printer.printBraceBlock("\(placeholder).map") { printer in + let inner = inner.render(&printer, "$0") + printer.print("return \(inner)") + } + return printer.finalize() + + case .unwrapOptional(let inner, let name, let fatalErrorMessage): + let unwrappedName = "\(name)_unwrapped$" + let inner = inner.render(&printer, placeholder) + printer.print( + """ + guard let \(unwrappedName) = \(inner) else { + fatalError("\(fatalErrorMessage)") + } + """ + ) + return unwrappedName + + case .asyncCompleteFuture( + let swiftFunctionResultType, + let nativeFunctionSignature, + let isThrowing, + let completeMethodID, + let completeExceptionallyMethodID + ): + var globalRefs: [String] = ["globalFuture"] + + // Global ref all indirect returns + for outParameter in nativeFunctionSignature.result.outParameters { + printer.print("let \(outParameter.name) = environment.interface.NewGlobalRef(environment, \(outParameter.name))") + globalRefs.append(outParameter.name) + } + + // We also need to global ref any objects passed in + for parameter in nativeFunctionSignature.parameters.flatMap(\.parameters) where !parameter.type.isPrimitive { + printer.print("let \(parameter.name) = environment.interface.NewGlobalRef(environment, \(parameter.name))") + globalRefs.append(parameter.name) + } + + printer.print( + """ + let globalFuture = environment.interface.NewGlobalRef(environment, result_future) + """ + ) + + func printDo(printer: inout CodePrinter) { + printer.print("let swiftResult$ = await \(placeholder)") + printer.print("environment = try! JavaVirtualMachine.shared().environment()") + let inner = nativeFunctionSignature.result.conversion.render(&printer, "swiftResult$") + if swiftFunctionResultType.isVoid { + printer.print("environment.interface.CallBooleanMethodA(environment, globalFuture, \(completeMethodID), [jvalue(l: nil)])") + } else { + let result: String + if nativeFunctionSignature.result.javaType.requiresBoxing { + printer.print("let boxedResult$ = SwiftJavaRuntimeSupport._JNIBoxedConversions.box(\(inner), in: environment)") + result = "boxedResult$" + } else { + result = inner + } + + printer.print("environment.interface.CallBooleanMethodA(environment, globalFuture, \(completeMethodID), [jvalue(l: \(result))])") + } + } + + func printTaskBody(printer: inout CodePrinter) { + printer.printBraceBlock("defer") { printer in + // Defer might on any thread, so we need to attach environment. + printer.print("let deferEnvironment = try! JavaVirtualMachine.shared().environment()") + for globalRef in globalRefs { + printer.print("deferEnvironment.interface.DeleteGlobalRef(deferEnvironment, \(globalRef))") + } + } + if isThrowing { + printer.printBraceBlock("do") { printer in + printDo(printer: &printer) + } + printer.printBraceBlock("catch") { printer in + // We might not be on the same thread after the suspension, so we need to attach the thread again. + printer.print( + """ + let catchEnvironment = try! JavaVirtualMachine.shared().environment() + let exception = catchEnvironment.interface.NewObjectA(catchEnvironment, _JNIMethodIDCache.Exception.class, _JNIMethodIDCache.Exception.constructWithMessage, [String(describing: error).getJValue(in: catchEnvironment)]) + catchEnvironment.interface.CallBooleanMethodA(catchEnvironment, globalFuture, \(completeExceptionallyMethodID), [jvalue(l: exception)]) + """ + ) + } + } else { + printDo(printer: &printer) + } + } + + printer.print("var task: Task? = nil") + printer.printHashIfBlock("swift(>=6.2)") { printer in + printer.printBraceBlock("if #available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, *)") { printer in + printer.printBraceBlock("task = Task.immediate") { printer in + // Immediate runs on the caller thread, so we don't need to attach the environment again. + printer.print("var environment = environment!") // this is to ensure we always use the same environment name, even though we are rebinding it. + printTaskBody(printer: &printer) + } + } + } + + printer.printBraceBlock("if task == nil") { printer in + printer.printBraceBlock("task = Task") { printer in + // We can be on any thread, so we need to attach the thread. + printer.print("var environment = try! JavaVirtualMachine.shared().environment()") + printTaskBody(printer: &printer) + } + } + + return "" + + case .closure(let args, let body): + var printer = CodePrinter() + printer.printBraceBlock("", parameters: args) { printer in + let body = body.render(&printer, placeholder) + if !body.isEmpty { + printer.print("return \(body)") + } + } + return printer.finalize() + } + } + } +} diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift new file mode 100644 index 000000000..13955430a --- /dev/null +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -0,0 +1,821 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JavaTypes +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +extension JNISwift2JavaGenerator { + func writeSwiftThunkSources() throws { + var printer = CodePrinter() + try writeSwiftThunkSources(&printer) + } + + package func writeSwiftExpectedEmptySources() throws { + let pendingFileCount = self.expectedOutputSwiftFiles.count + guard pendingFileCount > 0 else { + return // no need to write any empty files, yay + } + + print("[swift-java] Write empty [\(self.expectedOutputSwiftFiles.count)] 'expected' files in: \(swiftOutputDirectory)/") + + for expectedFileName in self.expectedOutputSwiftFiles { + logger.debug("Write SwiftPM-'expected' empty file: \(expectedFileName.bold)") + + + var printer = CodePrinter() + printer.print("// Empty file generated on purpose") + _ = try printer.writeContents( + outputDirectory: self.swiftOutputDirectory, + javaPackagePath: nil, + filename: expectedFileName) + } + } + + package func writeSwiftThunkSources(_ printer: inout CodePrinter) throws { + let moduleFilenameBase = "\(self.swiftModuleName)Module+SwiftJava" + let moduleFilename = "\(moduleFilenameBase).swift" + + do { + logger.trace("Printing swift module class: \(moduleFilename)") + + try printGlobalSwiftThunkSources(&printer) + + if let outputFile = try printer.writeContents( + outputDirectory: self.swiftOutputDirectory, + javaPackagePath: nil, + filename: moduleFilename + ) { + logger.info("Generated: \(moduleFilenameBase.bold).swift (at \(outputFile.absoluteString))") + self.expectedOutputSwiftFiles.remove(moduleFilename) + } + + // === All types + // We have to write all types to their corresponding output file that matches the file they were declared in, + // because otherwise SwiftPM plugins will not pick up files apropriately -- we expect 1 output +SwiftJava.swift file for every input. + for group: (key: String, value: [Dictionary.Element]) in Dictionary(grouping: self.analysis.importedTypes, by: { $0.value.sourceFilePath }) { + logger.warning("Writing types in file group: \(group.key): \(group.value.map(\.key))") + + let importedTypesForThisFile = group.value + .map(\.value) + .sorted(by: { $0.qualifiedName < $1.qualifiedName }) + + let inputFileName = "\(group.key)".split(separator: "/").last ?? "__Unknown.swift" + let filename = "\(inputFileName)".replacing(".swift", with: "+SwiftJava.swift") + + for ty in importedTypesForThisFile { + logger.info("Printing Swift thunks for type: \(ty.qualifiedName.bold)") + printer.printSeparator("Thunks for \(ty.qualifiedName)") + + do { + try printNominalTypeThunks(&printer, ty) + } catch { + logger.warning("Failed to print to Swift thunks for type'\(ty.qualifiedName)' to '\(filename)', error: \(error)") + } + + } + + logger.warning("Write Swift thunks file: \(filename.bold)") + do { + if let outputFile = try printer.writeContents( + outputDirectory: self.swiftOutputDirectory, + javaPackagePath: nil, + filename: filename) { + logger.info("Done writing Swift thunks to: \(outputFile.absoluteString)") + self.expectedOutputSwiftFiles.remove(filename) + } + } catch { + logger.warning("Failed to write to Swift thunks: \(filename), error: \(error)") + } + } + } catch { + logger.warning("Failed to write to Swift thunks: \(moduleFilename)") + } + } + + private func printJNICache(_ printer: inout CodePrinter, _ type: ImportedNominalType) { + printer.printBraceBlock("enum \(JNICaching.cacheName(for: type))") { printer in + for enumCase in type.cases { + guard let translatedCase = translatedEnumCase(for: enumCase) else { continue } + printer.print("static let \(JNICaching.cacheMemberName(for: enumCase)) = \(renderEnumCaseCacheInit(translatedCase))") + } + } + } + + /// Prints the extension needed to make allow upcalls from Swift to Java for protocols + private func printSwiftInterfaceWrapper( + _ printer: inout CodePrinter, + _ translatedWrapper: JavaInterfaceSwiftWrapper + ) throws { + printer.printBraceBlock("protocol \(translatedWrapper.wrapperName): \(translatedWrapper.swiftName)") { printer in + printer.print("var \(translatedWrapper.javaInterfaceVariableName): \(translatedWrapper.javaInterfaceName) { get }") + } + printer.println() + printer.printBraceBlock("extension \(translatedWrapper.wrapperName)") { printer in + for function in translatedWrapper.functions { + printInterfaceWrapperFunctionImpl(&printer, function, inside: translatedWrapper) + printer.println() + } + + // FIXME: Add support for protocol variables https://github.com/swiftlang/swift-java/issues/457 +// for variable in translatedWrapper.variables { +// printerInterfaceWrapperVariable(&printer, variable, inside: translatedWrapper) +// printer.println() +// } + } + } + + private func printInterfaceWrapperFunctionImpl( + _ printer: inout CodePrinter, + _ function: JavaInterfaceSwiftWrapper.Function, + inside wrapper: JavaInterfaceSwiftWrapper + ) { + printer.printBraceBlock(function.swiftDecl.signatureString) { printer in + let upcallArguments = zip( + function.originalFunctionSignature.parameters, + function.parameterConversions + ).map { param, conversion in + // Wrap-java does not extract parameter names, so no labels + conversion.render(&printer, param.parameterName!) + } + + let javaUpcall = "\(wrapper.javaInterfaceVariableName).\(function.swiftFunctionName)(\(upcallArguments.joined(separator: ", ")))" + + let resultType = function.originalFunctionSignature.result.type + let result = function.resultConversion.render(&printer, javaUpcall) + if resultType.isVoid { + printer.print(result) + } else { + printer.print("return \(result)") + } + } + } + + private func printerInterfaceWrapperVariable( + _ printer: inout CodePrinter, + _ variable: JavaInterfaceSwiftWrapper.Variable, + inside wrapper: JavaInterfaceSwiftWrapper + ) { + // FIXME: Add support for variables. This won't get printed yet + // so we no need to worry about fatalErrors. + printer.printBraceBlock(variable.swiftDecl.signatureString) { printer in + printer.printBraceBlock("get") { printer in + printer.print("fatalError()") + } + + if let setter = variable.setter { + printer.printBraceBlock("set") { printer in + printer.print("fatalError()") + } + } + } + } + + private func printGlobalSwiftThunkSources(_ printer: inout CodePrinter) throws { + printHeader(&printer) + + printJNIOnLoad(&printer) + + for decl in analysis.importedGlobalFuncs { + printSwiftFunctionThunk(&printer, decl) + printer.println() + } + + for decl in analysis.importedGlobalVariables { + printSwiftFunctionThunk(&printer, decl) + printer.println() + } + } + + private func printJNIOnLoad(_ printer: inout CodePrinter) { + printer.print( + """ + @_cdecl("JNI_OnLoad") + func JNI_OnLoad(javaVM: JavaVMPointer, reserved: UnsafeMutableRawPointer) -> jint { + SwiftJavaRuntimeSupport._JNI_OnLoad(javaVM, reserved) + return JNI_VERSION_1_6 + } + """ + ) + } + + private func printNominalTypeThunks(_ printer: inout CodePrinter, _ type: ImportedNominalType) throws { + printHeader(&printer) + + printJNICache(&printer, type) + printer.println() + + switch type.swiftNominal.kind { + case .actor, .class, .enum, .struct: + printConcreteTypeThunks(&printer, type) + case .protocol: + try printProtocolThunks(&printer, type) + } + } + + private func printConcreteTypeThunks(_ printer: inout CodePrinter, _ type: ImportedNominalType) { + for initializer in type.initializers { + printSwiftFunctionThunk(&printer, initializer) + printer.println() + } + + if type.swiftNominal.kind == .enum { + printEnumDiscriminator(&printer, type) + printer.println() + + for enumCase in type.cases { + printEnumCase(&printer, enumCase) + printer.println() + } + } + + for method in type.methods { + printSwiftFunctionThunk(&printer, method) + printer.println() + } + + for variable in type.variables { + printSwiftFunctionThunk(&printer, variable) + printer.println() + } + + printToStringMethods(&printer, type) + printTypeMetadataAddressThunk(&printer, type) + printer.println() + printDestroyFunctionThunk(&printer, type) + } + + private func printProtocolThunks(_ printer: inout CodePrinter, _ type: ImportedNominalType) throws { + guard let protocolWrapper = self.interfaceProtocolWrappers[type] else { + return + } + + try printSwiftInterfaceWrapper(&printer, protocolWrapper) + } + + private func printToStringMethods(_ printer: inout CodePrinter, _ type: ImportedNominalType) { + let selfPointerParam = JavaParameter(name: "selfPointer", type: .long) + let parentName = type.qualifiedName + + printCDecl( + &printer, + javaMethodName: "$toString", + parentName: type.swiftNominal.qualifiedName, + parameters: [ + selfPointerParam + ], + resultType: .javaLangString + ) { printer in + let selfVar = self.printSelfJLongToUnsafeMutablePointer(&printer, swiftParentName: parentName, selfPointerParam) + + printer.print( + """ + return String(describing: \(selfVar).pointee).getJNIValue(in: environment) + """ + ) + } + + printer.println() + + printCDecl( + &printer, + javaMethodName: "$toDebugString", + parentName: type.swiftNominal.qualifiedName, + parameters: [ + selfPointerParam + ], + resultType: .javaLangString + ) { printer in + let selfVar = self.printSelfJLongToUnsafeMutablePointer(&printer, swiftParentName: parentName, selfPointerParam) + + printer.print( + """ + return String(reflecting: \(selfVar).pointee).getJNIValue(in: environment) + """ + ) + } + } + + private func printEnumDiscriminator(_ printer: inout CodePrinter, _ type: ImportedNominalType) { + let selfPointerParam = JavaParameter(name: "selfPointer", type: .long) + printCDecl( + &printer, + javaMethodName: "$getDiscriminator", + parentName: type.swiftNominal.name, + parameters: [selfPointerParam], + resultType: .int + ) { printer in + let selfPointer = self.printSelfJLongToUnsafeMutablePointer( + &printer, + swiftParentName: type.swiftNominal.name, + selfPointerParam + ) + printer.printBraceBlock("switch (\(selfPointer).pointee)") { printer in + for (idx, enumCase) in type.cases.enumerated() { + printer.print("case .\(enumCase.name): return \(idx)") + } + } + } + } + + private func printEnumCase(_ printer: inout CodePrinter, _ enumCase: ImportedEnumCase) { + guard let translatedCase = self.translatedEnumCase(for: enumCase) else { + return + } + + // Print static case initializer + printSwiftFunctionThunk(&printer, enumCase.caseFunction) + printer.println() + + // Print getAsCase method + if !translatedCase.translatedValues.isEmpty { + printEnumGetAsCaseThunk(&printer, translatedCase) + } + } + + private func renderEnumCaseCacheInit(_ enumCase: TranslatedEnumCase) -> String { + let nativeParametersClassName = "\(enumCase.enumName)$\(enumCase.name)$_NativeParameters" + let methodSignature = MethodSignature(resultType: .void, parameterTypes: enumCase.parameterConversions.map(\.native.javaType)) + + return renderJNICacheInit(className: nativeParametersClassName, methods: [("", methodSignature)]) + } + + private func renderJNICacheInit(className: String, methods: [(String, MethodSignature)]) -> String { + let fullClassName = "\(javaPackagePath)/\(className)" + let methods = methods.map { name, signature in + #".init(name: "\#(name)", signature: "\#(signature.mangledName)")"# + }.joined(separator: ",\n") + + return #"_JNIMethodIDCache(className: "\#(fullClassName)", methods: [\#(methods)])"# + } + + private func printEnumGetAsCaseThunk( + _ printer: inout CodePrinter, + _ enumCase: TranslatedEnumCase + ) { + printCDecl( + &printer, + enumCase.getAsCaseFunction + ) { printer in + let selfPointer = enumCase.getAsCaseFunction.nativeFunctionSignature.selfParameter!.conversion.render(&printer, "self") + let caseNames = enumCase.original.parameters.enumerated().map { idx, parameter in + parameter.name ?? "_\(idx)" + } + let caseNamesWithLet = caseNames.map { "let \($0)" } + let methodSignature = MethodSignature(resultType: .void, parameterTypes: enumCase.parameterConversions.map(\.native.javaType)) + printer.print( + """ + guard case .\(enumCase.original.name)(\(caseNamesWithLet.joined(separator: ", "))) = \(selfPointer).pointee else { + fatalError("Expected enum case '\(enumCase.original.name)', but was '\\(\(selfPointer).pointee)'!") + } + let cache$ = \(JNICaching.cacheName(for: enumCase.original.enumType)).\(JNICaching.cacheMemberName(for: enumCase.original)) + let class$ = cache$.javaClass + let method$ = _JNIMethodIDCache.Method(name: "", signature: "\(methodSignature.mangledName)") + let constructorID$ = cache$[method$] + """ + ) + let upcallArguments = zip(enumCase.parameterConversions, caseNames).map { conversion, caseName in + let nullConversion = !conversion.native.javaType.isPrimitive ? " ?? nil" : "" + let result = conversion.native.conversion.render(&printer, caseName) + return "jvalue(\(conversion.native.javaType.jniFieldName): \(result)\(nullConversion))" + } + printer.print( + """ + let newObjectArgs$: [jvalue] = [\(upcallArguments.joined(separator: ", "))] + return environment.interface.NewObjectA(environment, class$, constructorID$, newObjectArgs$) + """ + ) + } + } + + private func printSwiftFunctionThunk( + _ printer: inout CodePrinter, + _ decl: ImportedFunc + ) { + guard let translatedDecl = translatedDecl(for: decl) else { + // Failed to translate. Skip. + return + } + + printSwiftFunctionHelperClasses(&printer, decl) + + printCDecl( + &printer, + translatedDecl + ) { printer in + self.printFunctionDowncall(&printer, decl) + } + } + + + private func printSwiftFunctionHelperClasses( + _ printer: inout CodePrinter, + _ decl: ImportedFunc + ) { + let protocolParameters = decl.functionSignature.parameters.compactMap { parameter in + if let concreteType = parameter.type.typeIn( + genericParameters: decl.functionSignature.genericParameters, + genericRequirements: decl.functionSignature.genericRequirements + ) { + return (parameter, concreteType) + } + + switch parameter.type { + case .opaque(let protocolType), + .existential(let protocolType): + return (parameter, protocolType) + + default: + return nil + } + }.map { parameter, protocolType in + // We flatten any composite types + switch protocolType { + case .composite(let protocols): + return (parameter, protocols) + + default: + return (parameter, [protocolType]) + } + } + + // For each parameter that is a generic or a protocol, + // we generate a Swift class that conforms to all of those. + for (parameter, protocolTypes) in protocolParameters { + let protocolWrappers: [JavaInterfaceSwiftWrapper] = protocolTypes.compactMap { protocolType in + guard let importedType = self.asImportedNominalTypeDecl(protocolType), + let wrapper = self.interfaceProtocolWrappers[importedType] + else { + return nil + } + return wrapper + } + + // Make sure we can generate wrappers for all the protocols + // that the parameter requires + guard protocolWrappers.count == protocolTypes.count else { + // We cannot extract a wrapper for this class + // so it must only be passed in by JExtract instances + continue + } + + guard let parameterName = parameter.parameterName else { + // TODO: Throw + fatalError() + } + let swiftClassName = JNISwift2JavaGenerator.protocolParameterWrapperClassName( + methodName: decl.name, + parameterName: parameterName, + parentName: decl.parentType?.asNominalType?.nominalTypeDecl.qualifiedName ?? swiftModuleName + ) + let implementingProtocols = protocolWrappers.map(\.wrapperName).joined(separator: ", ") + + printer.printBraceBlock("final class \(swiftClassName): \(implementingProtocols)") { printer in + let variables: [(String, String)] = protocolWrappers.map { wrapper in + return (wrapper.javaInterfaceVariableName, wrapper.javaInterfaceName) + } + for (name, type) in variables { + printer.print("let \(name): \(type)") + } + printer.println() + let initializerParameters = variables.map { "\($0): \($1)" }.joined(separator: ", ") + + printer.printBraceBlock("init(\(initializerParameters))") { printer in + for (name, _) in variables { + printer.print("self.\(name) = \(name)") + } + } + } + } + } + + private func asImportedNominalTypeDecl(_ type: SwiftType) -> ImportedNominalType? { + self.analysis.importedTypes.first(where: ( { name, nominalType in + nominalType.swiftType == type + })).map { + $0.value + } + } + + private func printFunctionDowncall( + _ printer: inout CodePrinter, + _ decl: ImportedFunc + ) { + guard let translatedDecl = self.translatedDecl(for: decl) else { + fatalError("Cannot print function downcall for a function that can't be translated: \(decl)") + } + let nativeSignature = translatedDecl.nativeFunctionSignature + + let tryClause: String = decl.isThrowing ? "try " : "" + + // Regular parameters. + var arguments = [String]() + for (idx, parameter) in nativeSignature.parameters.enumerated() { + let javaParameterName = translatedDecl.translatedFunctionSignature.parameters[idx].parameter.name + let lowered = parameter.conversion.render(&printer, javaParameterName) + arguments.append(lowered) + } + + // Callee + let callee: String = switch decl.functionSignature.selfParameter { + case .instance(let swiftSelf): + nativeSignature.selfParameter!.conversion.render( + &printer, + swiftSelf.parameterName ?? "self" + ) + case .staticMethod(let selfType), .initializer(let selfType): + "\(selfType)" + case .none: + swiftModuleName + } + + // Build the result + let result: String + switch decl.apiKind { + case .function, .initializer: + let downcallArguments = zip( + decl.functionSignature.parameters, + arguments + ).map { originalParam, argument in + let label = originalParam.argumentLabel.map { "\($0): " } ?? "" + return "\(label)\(argument)" + } + .joined(separator: ", ") + result = "\(tryClause)\(callee).\(decl.name)(\(downcallArguments))" + + case .enumCase: + let downcallArguments = zip( + decl.functionSignature.parameters, + arguments + ).map { originalParam, argument in + let label = originalParam.argumentLabel.map { "\($0): " } ?? "" + return "\(label)\(argument)" + } + + let associatedValues = !downcallArguments.isEmpty ? "(\(downcallArguments.joined(separator: ", ")))" : "" + result = "\(callee).\(decl.name)\(associatedValues)" + + case .getter: + result = "\(tryClause)\(callee).\(decl.name)" + + case .setter: + guard let newValueArgument = arguments.first else { + fatalError("Setter did not contain newValue parameter: \(decl)") + } + + result = "\(callee).\(decl.name) = \(newValueArgument)" + case .subscriptGetter: + let parameters = arguments.joined(separator: ", ") + result = "\(callee)[\(parameters)]" + case .subscriptSetter: + guard let newValueArgument = arguments.last else { + fatalError("Setter did not contain newValue parameter: \(decl)") + } + + var argumentsWithoutNewValue = arguments + argumentsWithoutNewValue.removeLast() + + let parameters = argumentsWithoutNewValue.joined(separator: ", ") + result = "\(callee)[\(parameters)] = \(newValueArgument)" + } + + // Lower the result. + func innerBody(in printer: inout CodePrinter) -> String { + let loweredResult = nativeSignature.result.conversion.render(&printer, result) + + if !decl.functionSignature.result.type.isVoid { + return "return \(loweredResult)" + } else { + return loweredResult + } + } + + if decl.isThrowing, !decl.isAsync { + let dummyReturn: String + + if nativeSignature.result.javaType.isVoid { + dummyReturn = "" + } else { + // We assume it is something that implements JavaValue + dummyReturn = "return \(nativeSignature.result.javaType.swiftTypeName(resolver: { _ in "" })).jniPlaceholderValue" + } + + printer.print("do {") + printer.indent() + printer.print(innerBody(in: &printer)) + printer.outdent() + printer.print("} catch {") + printer.indent() + printer.print( + """ + environment.throwAsException(error) + \(dummyReturn) + """ + ) + printer.outdent() + printer.print("}") + } else { + printer.print(innerBody(in: &printer)) + } + } + + private func printCDecl( + _ printer: inout CodePrinter, + _ translatedDecl: TranslatedFunctionDecl, + _ body: (inout CodePrinter) -> Void + ) { + let nativeSignature = translatedDecl.nativeFunctionSignature + var parameters = nativeSignature.parameters.flatMap(\.parameters) + + if let selfParameter = nativeSignature.selfParameter { + parameters += selfParameter.parameters + } + + parameters += nativeSignature.result.outParameters + + printCDecl( + &printer, + javaMethodName: translatedDecl.nativeFunctionName, + parentName: translatedDecl.parentName, + parameters: parameters, + resultType: nativeSignature.result.javaType + ) { printer in + body(&printer) + } + } + + private func printCDecl( + _ printer: inout CodePrinter, + javaMethodName: String, + parentName: String, + parameters: [JavaParameter], + resultType: JavaType, + _ body: (inout CodePrinter) -> Void + ) { + let jniSignature = parameters.reduce(into: "") { signature, parameter in + signature += parameter.type.jniTypeSignature + } + + let cName = + "Java_" + + self.javaPackage.replacingOccurrences(of: ".", with: "_") + + "_\(parentName.replacingOccurrences(of: ".", with: "$").escapedJNIIdentifier)_" + + javaMethodName.escapedJNIIdentifier + + "__" + + jniSignature.escapedJNIIdentifier + + let translatedParameters = parameters.map { + "\($0.name): \($0.type.jniTypeName)" + } + + let thunkParameters = + [ + "environment: UnsafeMutablePointer!", + "thisClass: jclass" + ] + translatedParameters + let thunkReturnType = resultType != .void ? " -> \(resultType.jniTypeName)" : "" + + // TODO: Think about function overloads + printer.printBraceBlock( + """ + @_cdecl("\(cName)") + func \(cName)(\(thunkParameters.joined(separator: ", ")))\(thunkReturnType) + """ + ) { printer in + body(&printer) + } + } + + private func printHeader(_ printer: inout CodePrinter) { + printer.print( + """ + // Generated by swift-java + + import SwiftJava + import CSwiftJavaJNI + import SwiftJavaRuntimeSupport + + """ + ) + } + + private func printTypeMetadataAddressThunk(_ printer: inout CodePrinter, _ type: ImportedNominalType) { + printCDecl( + &printer, + javaMethodName: "$typeMetadataAddressDowncall", + parentName: type.swiftNominal.qualifiedName, + parameters: [], + resultType: .long + ) { printer in + printer.print( + """ + let metadataPointer = unsafeBitCast(\(type.swiftNominal.qualifiedName).self, to: UnsafeRawPointer.self) + return Int64(Int(bitPattern: metadataPointer)).getJNIValue(in: environment) + """ + ) + } + } + + /// Prints the implementation of the destroy function. + private func printDestroyFunctionThunk(_ printer: inout CodePrinter, _ type: ImportedNominalType) { + let selfPointerParam = JavaParameter(name: "selfPointer", type: .long) + printCDecl( + &printer, + javaMethodName: "$destroy", + parentName: type.swiftNominal.qualifiedName, + parameters: [ + selfPointerParam + ], + resultType: .void + ) { printer in + let parentName = type.qualifiedName + let selfVar = self.printSelfJLongToUnsafeMutablePointer(&printer, swiftParentName: parentName, selfPointerParam) + // Deinitialize the pointer allocated (which will call the VWT destroy method) + // then deallocate the memory. + printer.print( + """ + \(selfVar).deinitialize(count: 1) + \(selfVar).deallocate() + """ + ) + } + } + + /// Print the necessary conversion logic to go from a `jlong` to a `UnsafeMutablePointer` + /// + /// - Returns: name of the created "self" variable + private func printSelfJLongToUnsafeMutablePointer( + _ printer: inout CodePrinter, + swiftParentName: String, + _ selfPointerParam: JavaParameter + ) -> String { + let newSelfParamName = "self$" + printer.print( + """ + guard let env$ = environment else { + fatalError("Missing JNIEnv in downcall to \\(#function)") + } + assert(\(selfPointerParam.name) != 0, "\(selfPointerParam.name) memory address was null") + let selfBits$ = Int(Int64(fromJNI: \(selfPointerParam.name), in: env$)) + guard let \(newSelfParamName) = UnsafeMutablePointer<\(swiftParentName)>(bitPattern: selfBits$) else { + fatalError("self memory address was null in call to \\(#function)!") + } + """ + ) + return newSelfParamName + } + + static func protocolParameterWrapperClassName( + methodName: String, + parameterName: String, + parentName: String? + ) -> String { + let parent = if let parentName { + "\(parentName)_" + } else { + "" + } + return "_\(parent)\(methodName)_\(parameterName)_Wrapper" + } +} + +extension SwiftNominalTypeDeclaration { + private var safeProtocolName: String { + self.qualifiedName.replacingOccurrences(of: ".", with: "_") + } + + /// The name of the corresponding `@JavaInterface` of this type. + var javaInterfaceName: String { + "Java\(safeProtocolName)" + } + + var javaInterfaceSwiftProtocolWrapperName: String { + "SwiftJava\(safeProtocolName)Wrapper" + } + + var javaInterfaceVariableName: String { + "_\(javaInterfaceName.firstCharacterLowercased)Interface" + } + + var generatedJavaClassMacroName: String { + if let parent { + return "\(parent.generatedJavaClassMacroName).Java\(self.name)" + } + + return "Java\(self.name)" + } +} diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift new file mode 100644 index 000000000..692b66b69 --- /dev/null +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift @@ -0,0 +1,101 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JavaTypes +import SwiftJavaConfigurationShared + +/// A table that where keys are Swift class names and the values are +/// the fully qualified canoical names. +package typealias JavaClassLookupTable = [String: String] + +package class JNISwift2JavaGenerator: Swift2JavaGenerator { + + let logger: Logger + let config: Configuration + let analysis: AnalysisResult + let swiftModuleName: String + let javaPackage: String + let swiftOutputDirectory: String + let javaOutputDirectory: String + let lookupContext: SwiftTypeLookupContext + + let javaClassLookupTable: JavaClassLookupTable + + var javaPackagePath: String { + javaPackage.replacingOccurrences(of: ".", with: "/") + } + + var thunkNameRegistry = ThunkNameRegistry() + + /// Cached Java translation result. 'nil' indicates failed translation. + var translatedDecls: [ImportedFunc: TranslatedFunctionDecl] = [:] + var translatedEnumCases: [ImportedEnumCase: TranslatedEnumCase] = [:] + var interfaceProtocolWrappers: [ImportedNominalType: JavaInterfaceSwiftWrapper] = [:] + + /// Because we need to write empty files for SwiftPM, keep track which files we didn't write yet, + /// and write an empty file for those. + var expectedOutputSwiftFiles: Set + + package init( + config: Configuration, + translator: Swift2JavaTranslator, + javaPackage: String, + swiftOutputDirectory: String, + javaOutputDirectory: String, + javaClassLookupTable: JavaClassLookupTable + ) { + self.config = config + self.logger = Logger(label: "jni-generator", logLevel: translator.log.logLevel) + self.analysis = translator.result + self.swiftModuleName = translator.swiftModuleName + self.javaPackage = javaPackage + self.swiftOutputDirectory = swiftOutputDirectory + self.javaOutputDirectory = javaOutputDirectory + self.javaClassLookupTable = javaClassLookupTable + self.lookupContext = translator.lookupContext + + // If we are forced to write empty files, construct the expected outputs + if translator.config.writeEmptyFiles ?? false { + self.expectedOutputSwiftFiles = Set(translator.inputs.compactMap { (input) -> String? in + guard let filePathPart = input.path.split(separator: "/\(translator.swiftModuleName)/").last else { + return nil + } + + return String(filePathPart.replacing(".swift", with: "+SwiftJava.swift")) + }) + self.expectedOutputSwiftFiles.insert("\(translator.swiftModuleName)Module+SwiftJava.swift") + self.expectedOutputSwiftFiles.insert("Foundation+SwiftJava.swift") + } else { + self.expectedOutputSwiftFiles = [] + } + + if translator.config.enableJavaCallbacks ?? false { + // We translate all the protocol wrappers + // as we need them to know what protocols we can allow the user to implement themselves + // in Java. + self.interfaceProtocolWrappers = self.generateInterfaceWrappers(Array(self.analysis.importedTypes.values)) + } + } + + func generate() throws { + try writeSwiftThunkSources() + try writeExportedJavaSources() + + let pendingFileCount = self.expectedOutputSwiftFiles.count + if pendingFileCount > 0 { + print("[swift-java] Write empty [\(pendingFileCount)] 'expected' files in: \(swiftOutputDirectory)/") + try writeSwiftExpectedEmptySources() + } + } +} diff --git a/Sources/JExtractSwiftLib/JavaParameter.swift b/Sources/JExtractSwiftLib/JavaParameter.swift new file mode 100644 index 000000000..0b243b3a4 --- /dev/null +++ b/Sources/JExtractSwiftLib/JavaParameter.swift @@ -0,0 +1,87 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JavaTypes + +/// Represent a parameter in Java code. +struct JavaParameter { + enum ParameterType: CustomStringConvertible { + case concrete(JavaType) + case generic(name: String, extends: [JavaType]) + + var jniTypeSignature: String { + switch self { + case .concrete(let type): + return type.jniTypeSignature + + case .generic(_, let extends): + guard !extends.isEmpty else { + return "Ljava/lang/Object;" + } + + // Generics only use the first type for JNI + return extends.first!.jniTypeSignature + } + } + + var isPrimitive: Bool { + switch self { + case .concrete(let javaType): + javaType.isPrimitive + case .generic(let name, let extends): + false + } + } + + var jniTypeName: String { + switch self { + case .concrete(let type): type.jniTypeName + case .generic: "jobject?" + } + } + + var description: String { + switch self { + case .concrete(let type): type.description + case .generic(let name, _): name + } + } + } + var name: String + var type: ParameterType + + /// Parameter annotations are used in parameter declarations like this: `@Annotation int example` + let annotations: [JavaAnnotation] + + init(name: String, type: ParameterType, annotations: [JavaAnnotation] = []) { + self.name = name + self.type = type + self.annotations = annotations + } + + init(name: String, type: JavaType, annotations: [JavaAnnotation] = []) { + self.name = name + self.type = .concrete(type) + self.annotations = annotations + } + + func renderParameter() -> String { + if annotations.isEmpty { + return "\(type) \(name)" + } + + let annotationsStr = annotations.map({$0.render()}).joined(separator: "") + return "\(annotationsStr) \(type) \(name)" + } +} diff --git a/Sources/JExtractSwiftLib/JavaConstants/JavaTypes.swift b/Sources/JExtractSwiftLib/JavaTypes/JavaType+JDK.swift similarity index 53% rename from Sources/JExtractSwiftLib/JavaConstants/JavaTypes.swift rename to Sources/JExtractSwiftLib/JavaTypes/JavaType+JDK.swift index 633943efd..e1aabd7fd 100644 --- a/Sources/JExtractSwiftLib/JavaConstants/JavaTypes.swift +++ b/Sources/JExtractSwiftLib/JavaTypes/JavaType+JDK.swift @@ -29,4 +29,30 @@ extension JavaType { static var javaLangRunnable: JavaType { .class(package: "java.lang", name: "Runnable") } + + /// The description of the type java.lang.Class. + static var javaLangClass: JavaType { + .class(package: "java.lang", name: "Class") + } + + /// The description of the type java.lang.Throwable. + static var javaLangThrowable: JavaType { + .class(package: "java.lang", name: "Throwable") + } + + /// The description of the type java.lang.Object. + static var javaLangObject: JavaType { + .class(package: "java.lang", name: "Object") + } + + + /// The description of the type java.util.concurrent.CompletableFuture + static func completableFuture(_ T: JavaType) -> JavaType { + .class(package: "java.util.concurrent", name: "CompletableFuture", typeParameters: [T.boxedType]) + } + + /// The description of the type java.util.concurrent.Future + static func future(_ T: JavaType) -> JavaType { + .class(package: "java.util.concurrent", name: "Future", typeParameters: [T.boxedType]) + } } diff --git a/Sources/JExtractSwiftLib/JavaTypes/JavaType+SwiftKit.swift b/Sources/JExtractSwiftLib/JavaTypes/JavaType+SwiftKit.swift new file mode 100644 index 000000000..7319b2942 --- /dev/null +++ b/Sources/JExtractSwiftLib/JavaTypes/JavaType+SwiftKit.swift @@ -0,0 +1,93 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JavaTypes + +extension JavaType { + + /// Try to map a Swift type name (e.g., from the module Swift) over to a + /// primitive Java type, or fail otherwise. + public init?(swiftTypeName: String, WHT_unsigned unsigned: UnsignedNumericsMode) { + switch swiftTypeName { + case "Bool": self = .boolean + + case "Int8": self = .byte + case "UInt8": + self = switch unsigned { + case .ignoreSign: .byte + case .wrapUnsignedGuava: JavaType.guava.primitives.UnsignedInteger + } + + case "Int16": self = .short + case "UInt16": self = .char + + case "Int32": self = .int + case "UInt32": + self = switch unsigned { + case .ignoreSign: .int + case .wrapUnsignedGuava: JavaType.guava.primitives.UnsignedInteger + } + + case "Int64": self = .long + case "UInt64": + self = switch unsigned { + case .ignoreSign: .long + case .wrapUnsignedGuava: JavaType.guava.primitives.UnsignedLong + } + + case "Float": self = .float + case "Double": self = .double + case "Void": self = .void + default: return nil + } + } +} + +extension JavaType { + + static func unsignedWrapper(for swiftType: SwiftType) -> JavaType? { + switch swiftType { + case .nominal(let nominal): + switch nominal.nominalTypeDecl.knownTypeKind { + case .uint8: return guava.primitives.UnsignedInteger + case .uint16: return .char // no wrapper necessary, we can express it as 'char' natively in Java + case .uint32: return guava.primitives.UnsignedInteger + case .uint64: return guava.primitives.UnsignedLong + default: return nil + } + default: return nil + } + } + + /// Known types from the Google Guava library + enum guava { + enum primitives { + static let package = "com.google.common.primitives" + + static var UnsignedInteger: JavaType { + .class(package: primitives.package, name: "UnsignedInteger") + } + + static var UnsignedLong: JavaType { + .class(package: primitives.package, name: "UnsignedLong") + } + } + } + + /// The description of the type org.swift.swiftkit.core.SimpleCompletableFuture + static func simpleCompletableFuture(_ T: JavaType) -> JavaType { + .class(package: "org.swift.swiftkit.core", name: "SimpleCompletableFuture", typeParameters: [T.boxedType]) + } + +} diff --git a/Sources/JExtractSwiftLib/Logger.swift b/Sources/JExtractSwiftLib/Logger.swift index 180ffb541..6ef046517 100644 --- a/Sources/JExtractSwiftLib/Logger.swift +++ b/Sources/JExtractSwiftLib/Logger.swift @@ -15,7 +15,7 @@ import Foundation import SwiftSyntax import ArgumentParser -import JavaKitConfigurationShared +import SwiftJavaConfigurationShared // Placeholder for some better logger, we could depend on swift-log public struct Logger { @@ -27,6 +27,23 @@ public struct Logger { self.logLevel = logLevel } + public func error( + _ message: @autoclosure () -> String, + metadata: [String: Any] = [:], + file: String = #fileID, + line: UInt = #line, + function: String = #function + ) { + guard logLevel <= .error else { + return + } + + let metadataString: String = + if metadata.isEmpty { "" } else { "\(metadata)" } + + print("[error][\(file):\(line)](\(function)) \(message()) \(metadataString)") + } + public func warning( _ message: @autoclosure () -> String, metadata: [String: Any] = [:], @@ -97,7 +114,7 @@ public struct Logger { } extension Logger { - public typealias Level = JavaKitConfigurationShared.LogLevel + public typealias Level = SwiftJavaConfigurationShared.LogLevel } extension Logger.Level: ExpressibleByArgument { @@ -111,7 +128,7 @@ extension Logger.Level: ExpressibleByArgument { } extension Logger.Level { - internal var naturalIntegralValue: Int { + var naturalIntegralValue: Int { switch self { case .trace: return 0 diff --git a/Sources/JExtractSwiftLib/Swift2Java.swift b/Sources/JExtractSwiftLib/Swift2Java.swift index 8adb76701..ed76483e3 100644 --- a/Sources/JExtractSwiftLib/Swift2Java.swift +++ b/Sources/JExtractSwiftLib/Swift2Java.swift @@ -15,14 +15,16 @@ import Foundation import SwiftSyntax import SwiftSyntaxBuilder -import JavaKitShared -import JavaKitConfigurationShared // TODO: this should become SwiftJavaConfigurationShared +import SwiftJavaShared +import SwiftJavaConfigurationShared public struct SwiftToJava { let config: Configuration + let dependentConfigs: [Configuration] - public init(config: Configuration) { + public init(config: Configuration, dependentConfigs: [Configuration]) { self.config = config + self.dependentConfigs = dependentConfigs } public func run() throws { @@ -30,21 +32,18 @@ public struct SwiftToJava { fatalError("Missing '--swift-module' name.") } - let translator = Swift2JavaTranslator( - swiftModuleName: swiftModule - ) + let translator = Swift2JavaTranslator(config: config) translator.log.logLevel = config.logLevel ?? .info if config.javaPackage == nil || config.javaPackage!.isEmpty { translator.log.warning("Configured java package is '', consider specifying concrete package for generated sources.") } - print("===== CONFIG ==== \(config)") - guard let inputSwift = config.inputSwiftDirectory else { fatalError("Missing '--swift-input' directory!") } + translator.log.info("Input swift = \(inputSwift)") let inputPaths = inputSwift.split(separator: ",").map { URL(string: String($0))! } translator.log.info("Input paths = \(inputPaths)") @@ -86,17 +85,38 @@ public struct SwiftToJava { fatalError("Missing --output-java directory!") } + let wrappedJavaClassesLookupTable: JavaClassLookupTable = dependentConfigs.compactMap(\.classes).reduce(into: [:]) { + for (canonicalName, javaClass) in $1 { + $0[javaClass] = canonicalName + } + } + + translator.dependenciesClasses = Array(wrappedJavaClassesLookupTable.keys) + try translator.analyze() - switch config.mode { - case .some(.ffm), .none: + switch config.effectiveMode { + case .ffm: let generator = FFMSwift2JavaGenerator( + config: self.config, translator: translator, javaPackage: config.javaPackage ?? "", swiftOutputDirectory: outputSwiftDirectory, javaOutputDirectory: outputJavaDirectory ) + try generator.generate() + + case .jni: + let generator = JNISwift2JavaGenerator( + config: self.config, + translator: translator, + javaPackage: config.javaPackage ?? "", + swiftOutputDirectory: outputSwiftDirectory, + javaOutputDirectory: outputJavaDirectory, + javaClassLookupTable: wrappedJavaClassesLookupTable + ) + try generator.generate() } @@ -104,8 +124,15 @@ public struct SwiftToJava { } func canExtract(from file: URL) -> Bool { - file.lastPathComponent.hasSuffix(".swift") || - file.lastPathComponent.hasSuffix(".swiftinterface") + guard file.lastPathComponent.hasSuffix(".swift") || + file.lastPathComponent.hasSuffix(".swiftinterface") else { + return false + } + if file.lastPathComponent.hasSuffix("+SwiftJava.swift") { + return false + } + + return true } } diff --git a/Sources/JExtractSwiftLib/Swift2JavaTranslator.swift b/Sources/JExtractSwiftLib/Swift2JavaTranslator.swift index ace5f4444..60b99a632 100644 --- a/Sources/JExtractSwiftLib/Swift2JavaTranslator.swift +++ b/Sources/JExtractSwiftLib/Swift2JavaTranslator.swift @@ -16,22 +16,26 @@ import Foundation import JavaTypes import SwiftBasicFormat import SwiftParser +import SwiftJavaConfigurationShared import SwiftSyntax /// Takes swift interfaces and translates them into Java used to access those. public final class Swift2JavaTranslator { static let SWIFT_INTERFACE_SUFFIX = ".swiftinterface" - package var log = Logger(label: "translator", logLevel: .info) + package var log: Logger + + let config: Configuration + + /// The name of the Swift module being translated. + let swiftModuleName: String // ==== Input - struct Input { - let filePath: String - let syntax: SourceFileSyntax - } + var inputs: [SwiftJavaInputFile] = [] - var inputs: [Input] = [] + /// A list of used Swift class names that live in dependencies, e.g. `JavaInteger` + package var dependenciesClasses: [String] = [] // ==== Output state @@ -43,24 +47,21 @@ public final class Swift2JavaTranslator { /// type representation. package var importedTypes: [String: ImportedNominalType] = [:] - package var swiftStdlibTypes: SwiftStandardLibraryTypes + var lookupContext: SwiftTypeLookupContext! = nil - package let symbolTable: SwiftSymbolTable - - /// The name of the Swift module being translated. - var swiftModuleName: String { - symbolTable.moduleName + var symbolTable: SwiftSymbolTable! { + return lookupContext?.symbolTable } public init( - swiftModuleName: String + config: Configuration ) { - self.symbolTable = SwiftSymbolTable(parsedModuleName: swiftModuleName) - - // Create a mock of the Swift standard library. - var parsedSwiftModule = SwiftParsedModuleSymbolTable(moduleName: "Swift") - self.swiftStdlibTypes = SwiftStandardLibraryTypes(into: &parsedSwiftModule) - self.symbolTable.importedModules.append(parsedSwiftModule.symbolTable) + guard let swiftModule = config.swiftModule else { + fatalError("Missing 'swiftModule' name.") // FIXME: can we make it required in config? but we shared config for many cases + } + self.log = Logger(label: "translator", logLevel: config.logLevel ?? .info) + self.config = config + self.swiftModuleName = swiftModule } } @@ -79,15 +80,12 @@ extension Swift2JavaTranslator { package func add(filePath: String, text: String) { log.trace("Adding: \(filePath)") let sourceFileSyntax = Parser.parse(source: text) - self.inputs.append(Input(filePath: filePath, syntax: sourceFileSyntax)) + self.inputs.append(SwiftJavaInputFile(syntax: sourceFileSyntax, path: filePath)) } /// Convenient method for analyzing single file. - package func analyze( - file: String, - text: String - ) throws { - self.add(filePath: file, text: text) + package func analyze(path: String, text: String) throws { + self.add(filePath: path, text: text) try self.analyze() } @@ -98,14 +96,96 @@ extension Swift2JavaTranslator { let visitor = Swift2JavaVisitor(translator: self) for input in self.inputs { - log.trace("Analyzing \(input.filePath)") - visitor.walk(input.syntax) + log.trace("Analyzing \(input.path)") + visitor.visit(inputFile: input) + } + + // If any API uses 'Foundation.Data' or 'FoundationEssentials.Data', + // import 'Data' as if it's declared in this module. + if let dataDecl = self.symbolTable[.foundationData] ?? self.symbolTable[.essentialsData] { + let dataProtocolDecl = (self.symbolTable[.foundationDataProtocol] ?? self.symbolTable[.essentialsDataProtocol])! + if self.isUsing(where: { $0 == dataDecl || $0 == dataProtocolDecl }) { + visitor.visit(nominalDecl: dataDecl.syntax!.asNominal!, in: nil, sourceFilePath: "Foundation/FAKE_FOUNDATION_DATA.swift") + } } } package func prepareForTranslation() { - /// Setup the symbol table. - symbolTable.setup(inputs.map({ $0.syntax })) + let dependenciesSource = self.buildDependencyClassesSourceFile() + + let symbolTable = SwiftSymbolTable.setup( + moduleName: self.swiftModuleName, + inputs + [dependenciesSource], + log: self.log + ) + self.lookupContext = SwiftTypeLookupContext(symbolTable: symbolTable) + } + + /// Check if any of the imported decls uses a nominal declaration that satisfies + /// the given predicate. + func isUsing(where predicate: (SwiftNominalTypeDeclaration) -> Bool) -> Bool { + func check(_ type: SwiftType) -> Bool { + switch type { + case .nominal(let nominal): + return predicate(nominal.nominalTypeDecl) + case .optional(let ty): + return check(ty) + case .tuple(let tuple): + return tuple.contains(where: check) + case .function(let fn): + return check(fn.resultType) || fn.parameters.contains(where: { check($0.type) }) + case .metatype(let ty): + return check(ty) + case .existential(let ty), .opaque(let ty): + return check(ty) + case .composite(let types): + return types.contains(where: check) + case .genericParameter: + return false + case .array(let ty): + return check(ty) + } + } + + func check(_ fn: ImportedFunc) -> Bool { + if check(fn.functionSignature.result.type) { + return true + } + if fn.functionSignature.parameters.contains(where: { check($0.type) }) { + return true + } + return false + } + + if self.importedGlobalFuncs.contains(where: check) { + return true + } + if self.importedGlobalVariables.contains(where: check) { + return true + } + for importedType in self.importedTypes.values { + if importedType.initializers.contains(where: check) { + return true + } + if importedType.methods.contains(where: check) { + return true + } + if importedType.variables.contains(where: check) { + return true + } + } + return false + } + + /// Returns a source file that contains all the available dependency classes. + private func buildDependencyClassesSourceFile() -> SwiftJavaInputFile { + let contents = self.dependenciesClasses.map { + "public class \($0) {}" + } + .joined(separator: "\n") + + let syntax = SourceFileSyntax(stringLiteral: contents) + return SwiftJavaInputFile(syntax: syntax, path: "FakeDependencyClassesSourceFile.swift") } } @@ -117,7 +197,7 @@ extension Swift2JavaTranslator { _ nominalNode: some DeclGroupSyntax & NamedDeclSyntax & WithModifiersSyntax & WithAttributesSyntax, parent: ImportedNominalType? ) -> ImportedNominalType? { - if !nominalNode.shouldImport(log: log) { + if !nominalNode.shouldExtract(config: config, log: log, in: parent) { return nil } @@ -131,7 +211,7 @@ extension Swift2JavaTranslator { func importedNominalType( _ typeNode: TypeSyntax ) -> ImportedNominalType? { - guard let swiftType = try? SwiftType(typeNode, symbolTable: self.symbolTable) else { + guard let swiftType = try? SwiftType(typeNode, lookupContext: lookupContext) else { return nil } guard let swiftNominalDecl = swiftType.asNominalTypeDeclaration else { @@ -139,10 +219,10 @@ extension Swift2JavaTranslator { } // Whether to import this extension? - guard let nominalNode = symbolTable.parsedModule.nominalTypeSyntaxNodes[swiftNominalDecl] else { + guard swiftNominalDecl.moduleName == self.swiftModuleName else { return nil } - guard nominalNode.shouldImport(log: log) else { + guard swiftNominalDecl.syntax!.shouldExtract(config: config, log: log, in: nil) else { return nil } @@ -156,7 +236,7 @@ extension Swift2JavaTranslator { return alreadyImported } - let importedNominal = ImportedNominalType(swiftNominal: nominal) + let importedNominal = try? ImportedNominalType(swiftNominal: nominal, lookupContext: lookupContext) importedTypes[fullName] = importedNominal return importedNominal diff --git a/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift b/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift index 0b4d6bbf0..cf99d11b6 100644 --- a/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift +++ b/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Copyright (c) 2024-2025 Apple Inc. and the Swift.org project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -13,101 +13,120 @@ //===----------------------------------------------------------------------===// import Foundation +import SwiftJavaConfigurationShared import SwiftParser import SwiftSyntax -final class Swift2JavaVisitor: SyntaxVisitor { +final class Swift2JavaVisitor { let translator: Swift2JavaTranslator - - /// Type context stack associated with the syntax. - var typeContext: [(syntaxID: Syntax.ID, type: ImportedNominalType)] = [] - - /// Innermost type context. - var currentType: ImportedNominalType? { typeContext.last?.type } - - var currentSwiftType: SwiftType? { - guard let currentType else { return nil } - return .nominal(SwiftNominalType(nominalTypeDecl: currentType.swiftNominal)) + var config: Configuration { + self.translator.config } - /// The current type name as a nested name like A.B.C. - var currentTypeName: String? { self.currentType?.swiftNominal.qualifiedName } - - var log: Logger { translator.log } - init(translator: Swift2JavaTranslator) { self.translator = translator - - super.init(viewMode: .all) } - /// Push specified type to the type context associated with the syntax. - func pushTypeContext(syntax: some SyntaxProtocol, importedNominal: ImportedNominalType) { - typeContext.append((syntax.id, importedNominal)) - } + var log: Logger { translator.log } - /// Pop type context if the current context is associated with the syntax. - func popTypeContext(syntax: some SyntaxProtocol) -> Bool { - if typeContext.last?.syntaxID == syntax.id { - typeContext.removeLast() - return true - } else { - return false + func visit(inputFile: SwiftJavaInputFile) { + let node = inputFile.syntax + for codeItem in node.statements { + if let declNode = codeItem.item.as(DeclSyntax.self) { + self.visit(decl: declNode, in: nil, sourceFilePath: inputFile.path) + } } } - override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind { - log.debug("Visit \(node.kind): '\(node.qualifiedNameForDebug)'") - guard let importedNominalType = translator.importedNominalType(node, parent: self.currentType) else { - return .skipChildren + func visit(decl node: DeclSyntax, in parent: ImportedNominalType?, sourceFilePath: String) { + switch node.as(DeclSyntaxEnum.self) { + case .actorDecl(let node): + self.visit(nominalDecl: node, in: parent, sourceFilePath: sourceFilePath) + case .classDecl(let node): + self.visit(nominalDecl: node, in: parent, sourceFilePath: sourceFilePath) + case .structDecl(let node): + self.visit(nominalDecl: node, in: parent, sourceFilePath: sourceFilePath) + case .enumDecl(let node): + self.visit(enumDecl: node, in: parent, sourceFilePath: sourceFilePath) + case .protocolDecl(let node): + self.visit(nominalDecl: node, in: parent, sourceFilePath: sourceFilePath) + case .extensionDecl(let node): + self.visit(extensionDecl: node, in: parent, sourceFilePath: sourceFilePath) + case .typeAliasDecl: + break // TODO: Implement; https://github.com/swiftlang/swift-java/issues/338 + case .associatedTypeDecl: + break // TODO: Implement associated types + + case .initializerDecl(let node): + self.visit(initializerDecl: node, in: parent) + case .functionDecl(let node): + self.visit(functionDecl: node, in: parent, sourceFilePath: sourceFilePath) + case .variableDecl(let node): + self.visit(variableDecl: node, in: parent, sourceFilePath: sourceFilePath) + case .subscriptDecl(let node): + self.visit(subscriptDecl: node, in: parent) + case .enumCaseDecl(let node): + self.visit(enumCaseDecl: node, in: parent) + + default: + break } - - self.pushTypeContext(syntax: node, importedNominal: importedNominalType) - return .visitChildren } - override func visitPost(_ node: ClassDeclSyntax) { - if self.popTypeContext(syntax: node) { - log.debug("Completed import: \(node.kind) \(node.name)") + func visit( + nominalDecl node: some DeclSyntaxProtocol & DeclGroupSyntax & NamedDeclSyntax + & WithAttributesSyntax & WithModifiersSyntax, + in parent: ImportedNominalType?, + sourceFilePath: String + ) { + guard let importedNominalType = translator.importedNominalType(node, parent: parent) else { + return + } + for memberItem in node.memberBlock.members { + self.visit(decl: memberItem.decl, in: importedNominalType, sourceFilePath: sourceFilePath) } } - override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind { - log.debug("Visit \(node.kind): \(node.qualifiedNameForDebug)") - guard let importedNominalType = translator.importedNominalType(node, parent: self.currentType) else { - return .skipChildren - } + func visit( + enumDecl node: EnumDeclSyntax, + in parent: ImportedNominalType?, + sourceFilePath: String + ) { + self.visit(nominalDecl: node, in: parent, sourceFilePath: sourceFilePath) - self.pushTypeContext(syntax: node, importedNominal: importedNominalType) - return .visitChildren + self.synthesizeRawRepresentableConformance(enumDecl: node, in: parent) } - override func visitPost(_ node: StructDeclSyntax) { - if self.popTypeContext(syntax: node) { - log.debug("Completed import: \(node.kind) \(node.qualifiedNameForDebug)") + func visit( + extensionDecl node: ExtensionDeclSyntax, + in parent: ImportedNominalType?, + sourceFilePath: String + ) { + guard parent == nil else { + // 'extension' in a nominal type is invalid. Ignore + return } - } - - override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind { - // Resolve the extended type of the extension as an imported nominal, and - // recurse if we found it. guard let importedNominalType = translator.importedNominalType(node.extendedType) else { - return .skipChildren + return } - self.pushTypeContext(syntax: node, importedNominal: importedNominalType) - return .visitChildren - } + // Add any conforming protocols in the extension + importedNominalType.inheritedTypes += node.inheritanceClause?.inheritedTypes.compactMap { + try? SwiftType($0.type, lookupContext: translator.lookupContext) + } ?? [] - override func visitPost(_ node: ExtensionDeclSyntax) { - if self.popTypeContext(syntax: node) { - log.debug("Completed import: \(node.kind) \(node.qualifiedNameForDebug)") + for memberItem in node.memberBlock.members { + self.visit(decl: memberItem.decl, in: importedNominalType, sourceFilePath: sourceFilePath) } } - override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind { - guard node.shouldImport(log: log) else { - return .skipChildren + func visit( + functionDecl node: FunctionDeclSyntax, + in typeContext: ImportedNominalType?, + sourceFilePath: String + ) { + guard node.shouldExtract(config: config, log: log, in: typeContext) else { + return } self.log.debug("Import function: '\(node.qualifiedNameForDebug)'") @@ -116,12 +135,12 @@ final class Swift2JavaVisitor: SyntaxVisitor { do { signature = try SwiftFunctionSignature( node, - enclosingType: self.currentSwiftType, - symbolTable: self.translator.symbolTable + enclosingType: typeContext?.swiftType, + lookupContext: translator.lookupContext ) } catch { self.log.debug("Failed to import: '\(node.qualifiedNameForDebug)'; \(error)") - return .skipChildren + return } let imported = ImportedFunc( @@ -133,74 +152,107 @@ final class Swift2JavaVisitor: SyntaxVisitor { ) log.debug("Record imported method \(node.qualifiedNameForDebug)") - if let currentType { - currentType.methods.append(imported) + if let typeContext { + typeContext.methods.append(imported) } else { translator.importedGlobalFuncs.append(imported) } + } - return .skipChildren + func visit( + enumCaseDecl node: EnumCaseDeclSyntax, + in typeContext: ImportedNominalType? + ) { + guard let typeContext else { + self.log.info("Enum case must be within a current type; \(node)") + return + } + + do { + for caseElement in node.elements { + self.log.debug("Import case \(caseElement.name) of enum \(node.qualifiedNameForDebug)") + + let parameters = try caseElement.parameterClause?.parameters.map { + try SwiftEnumCaseParameter($0, lookupContext: translator.lookupContext) + } + + let signature = try SwiftFunctionSignature( + caseElement, + enclosingType: typeContext.swiftType, + lookupContext: translator.lookupContext + ) + + let caseFunction = ImportedFunc( + module: translator.swiftModuleName, + swiftDecl: node, + name: caseElement.name.text, + apiKind: .enumCase, + functionSignature: signature + ) + + let importedCase = ImportedEnumCase( + name: caseElement.name.text, + parameters: parameters ?? [], + swiftDecl: node, + enumType: SwiftNominalType(nominalTypeDecl: typeContext.swiftNominal), + caseFunction: caseFunction + ) + + typeContext.cases.append(importedCase) + } + } catch { + self.log.debug("Failed to import: \(node.qualifiedNameForDebug); \(error)") + } } - override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind { - guard node.shouldImport(log: log) else { - return .skipChildren + func visit( + variableDecl node: VariableDeclSyntax, + in typeContext: ImportedNominalType?, + sourceFilePath: String + ) { + guard node.shouldExtract(config: config, log: log, in: typeContext) else { + return } guard let binding = node.bindings.first else { - return .skipChildren + return } let varName = "\(binding.pattern.trimmed)" self.log.debug("Import variable: \(node.kind) '\(node.qualifiedNameForDebug)'") - func importAccessor(kind: SwiftAPIKind) throws { - let signature = try SwiftFunctionSignature( - node, - isSet: kind == .setter, - enclosingType: self.currentSwiftType, - symbolTable: self.translator.symbolTable - ) - - let imported = ImportedFunc( - module: translator.swiftModuleName, - swiftDecl: node, - name: varName, - apiKind: kind, - functionSignature: signature - ) - - log.debug("Record imported variable accessor \(kind == .getter ? "getter" : "setter"):\(node.qualifiedNameForDebug)") - if let currentType { - currentType.variables.append(imported) - } else { - translator.importedGlobalVariables.append(imported) - } - } - do { let supportedAccessors = node.supportedAccessorKinds(binding: binding) if supportedAccessors.contains(.get) { - try importAccessor(kind: .getter) + try importAccessor( + from: DeclSyntax(node), + in: typeContext, + kind: .getter, + name: varName) } if supportedAccessors.contains(.set) { - try importAccessor(kind: .setter) + try importAccessor( + from: DeclSyntax(node), + in: typeContext, + kind: .setter, + name: varName) } } catch { self.log.debug("Failed to import: \(node.qualifiedNameForDebug); \(error)") - return .skipChildren } - - return .skipChildren } - override func visit(_ node: InitializerDeclSyntax) -> SyntaxVisitorContinueKind { - guard let currentType else { - fatalError("Initializer must be within a current type, was: \(node)") + func visit( + initializerDecl node: InitializerDeclSyntax, + in typeContext: ImportedNominalType?, + ) { + guard let typeContext else { + self.log.info("Initializer must be within a current type; \(node)") + return } - guard node.shouldImport(log: log) else { - return .skipChildren + guard node.shouldExtract(config: config, log: log, in: typeContext) else { + return } self.log.debug("Import initializer: \(node.kind) '\(node.qualifiedNameForDebug)'") @@ -209,12 +261,12 @@ final class Swift2JavaVisitor: SyntaxVisitor { do { signature = try SwiftFunctionSignature( node, - enclosingType: self.currentSwiftType, - symbolTable: self.translator.symbolTable + enclosingType: typeContext.swiftType, + lookupContext: translator.lookupContext ) } catch { self.log.debug("Failed to import: \(node.qualifiedNameForDebug); \(error)") - return .skipChildren + return } let imported = ImportedFunc( module: translator.swiftModuleName, @@ -224,36 +276,142 @@ final class Swift2JavaVisitor: SyntaxVisitor { functionSignature: signature ) - currentType.initializers.append(imported) + typeContext.initializers.append(imported) + } + + private func visit( + subscriptDecl node: SubscriptDeclSyntax, + in typeContext: ImportedNominalType?, + ) { + guard node.shouldExtract(config: config, log: log, in: typeContext) else { + return + } + + guard let accessorBlock = node.accessorBlock else { + return + } + + let name = "subscript" + let accessors = accessorBlock.supportedAccessorKinds() - return .skipChildren + do { + if accessors.contains(.get) { + try importAccessor( + from: DeclSyntax(node), + in: typeContext, + kind: .subscriptGetter, + name: name) + } + if accessors.contains(.set) { + try importAccessor( + from: DeclSyntax(node), + in: typeContext, + kind: .subscriptSetter, + name: name) + } + } catch { + self.log.debug("Failed to import: \(node.qualifiedNameForDebug); \(error)") + } } - override func visit(_ node: DeinitializerDeclSyntax) -> SyntaxVisitorContinueKind { - return .skipChildren + private func importAccessor( + from node: DeclSyntax, + in typeContext: ImportedNominalType?, + kind: SwiftAPIKind, + name: String + ) throws { + let signature: SwiftFunctionSignature + + switch node.as(DeclSyntaxEnum.self) { + case .variableDecl(let varNode): + signature = try SwiftFunctionSignature( + varNode, + isSet: kind == .setter, + enclosingType: typeContext?.swiftType, + lookupContext: translator.lookupContext) + case .subscriptDecl(let subscriptNode): + signature = try SwiftFunctionSignature( + subscriptNode, + isSet: kind == .subscriptSetter, + enclosingType: typeContext?.swiftType, + lookupContext: translator.lookupContext) + default: + log.warning("Not supported declaration type \(node.kind) while calling importAccessor!") + return + } + + let imported = ImportedFunc( + module: translator.swiftModuleName, + swiftDecl: node, + name: name, + apiKind: kind, + functionSignature: signature + ) + + log.debug( + "Record imported variable accessor \(kind == .getter || kind == .subscriptGetter ? "getter" : "setter"):\(node.qualifiedNameForDebug)" + ) + if let typeContext { + typeContext.variables.append(imported) + } else { + translator.importedGlobalVariables.append(imported) + } + } + + private func synthesizeRawRepresentableConformance( + enumDecl node: EnumDeclSyntax, + in parent: ImportedNominalType? + ) { + guard let imported = translator.importedNominalType(node, parent: parent) else { + return + } + + if let firstInheritanceType = imported.swiftNominal.firstInheritanceType, + let inheritanceType = try? SwiftType( + firstInheritanceType, + lookupContext: translator.lookupContext + ), + inheritanceType.isRawTypeCompatible + { + if !imported.variables.contains(where: { + $0.name == "rawValue" && $0.functionSignature.result.type != inheritanceType + }) { + let decl: DeclSyntax = "public var rawValue: \(raw: inheritanceType.description) { get }" + self.visit(decl: decl, in: imported, sourceFilePath: imported.sourceFilePath) + } + + if !imported.initializers.contains(where: { + $0.functionSignature.parameters.count == 1 + && $0.functionSignature.parameters.first?.parameterName == "rawValue" + && $0.functionSignature.parameters.first?.type == inheritanceType + }) { + let decl: DeclSyntax = "public init?(rawValue: \(raw: inheritanceType))" + self.visit(decl: decl, in: imported, sourceFilePath: imported.sourceFilePath) + } + } } } extension DeclSyntaxProtocol where Self: WithModifiersSyntax & WithAttributesSyntax { - func shouldImport(log: Logger) -> Bool { - guard accessControlModifiers.contains(where: { $0.isPublic }) else { - log.trace("Skip import '\(self.qualifiedNameForDebug)': not public") + func shouldExtract(config: Configuration, log: Logger, in parent: ImportedNominalType?) -> Bool { + let meetsRequiredAccessLevel: Bool = + switch config.effectiveMinimumInputAccessLevelMode { + case .public: self.isPublic(in: parent?.swiftNominal.syntax) + case .package: self.isAtLeastPackage + case .internal: self.isAtLeastInternal + } + + guard meetsRequiredAccessLevel else { + log.debug( + "Skip import '\(self.qualifiedNameForDebug)': not at least \(config.effectiveMinimumInputAccessLevelMode)" + ) return false } guard !attributes.contains(where: { $0.isJava }) else { - log.trace("Skip import '\(self.qualifiedNameForDebug)': is Java") + log.debug("Skip import '\(self.qualifiedNameForDebug)': is Java") return false } - if let node = self.as(InitializerDeclSyntax.self) { - let isFailable = node.optionalMark != nil - - if isFailable { - log.warning("Skip import '\(self.qualifiedNameForDebug)': failable initializer") - return false - } - } - return true } } diff --git a/Sources/JExtractSwiftLib/SwiftKit+Printing.swift b/Sources/JExtractSwiftLib/SwiftKit+Printing.swift index aa01502d7..0afc96452 100644 --- a/Sources/JExtractSwiftLib/SwiftKit+Printing.swift +++ b/Sources/JExtractSwiftLib/SwiftKit+Printing.swift @@ -23,7 +23,7 @@ package struct SwiftKitPrinting { /// Forms syntax for a Java call to a swiftkit exposed function. static func renderCallGetSwiftType(module: String, nominal: ImportedNominalType) -> String { """ - SwiftKit.swiftjava.getType("\(module)", "\(nominal.swiftNominal.qualifiedName)") + SwiftRuntime.swiftjava.getType("\(module)", "\(nominal.swiftNominal.qualifiedName)") """ } } diff --git a/Sources/JExtractSwiftLib/SwiftTypes/DependencyScanner.swift b/Sources/JExtractSwiftLib/SwiftTypes/DependencyScanner.swift new file mode 100644 index 000000000..79abe9812 --- /dev/null +++ b/Sources/JExtractSwiftLib/SwiftTypes/DependencyScanner.swift @@ -0,0 +1,88 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftSyntax + +/// Scan importing modules. +func importingModules(sourceFile: SourceFileSyntax) -> [ImportedSwiftModule] { + var importingModuleNames: [ImportedSwiftModule] = [] + for item in sourceFile.statements { + if let importDecl = item.item.as(ImportDeclSyntax.self) { + guard let moduleName = importDecl.path.first?.name.text else { + continue + } + importingModuleNames.append(ImportedSwiftModule(name: moduleName, availableWithModuleName: nil, alternativeModuleNames: [])) + } else if let ifConfigDecl = item.item.as(IfConfigDeclSyntax.self) { + importingModuleNames.append(contentsOf: modules(from: ifConfigDecl)) + } + } + return importingModuleNames +} + +private func modules(from ifConfigDecl: IfConfigDeclSyntax) -> [ImportedSwiftModule] { + guard + let firstClause = ifConfigDecl.clauses.first, + let calledExpression = firstClause.condition?.as(FunctionCallExprSyntax.self)?.calledExpression.as(DeclReferenceExprSyntax.self), + calledExpression.baseName.text == "canImport" else { + return [] + } + + var modules: [ImportedSwiftModule] = [] + modules.reserveCapacity(ifConfigDecl.clauses.count) + + for (index, clause) in ifConfigDecl.clauses.enumerated() { + let importedModuleNames = clause.elements?.as(CodeBlockItemListSyntax.self)? + .compactMap { CodeBlockItemSyntax($0) } + .compactMap { $0.item.as(ImportDeclSyntax.self) } + .compactMap { $0.path.first?.name.text } ?? [] + + let importModuleName: String? = if + let funcCallExpr = clause.condition?.as(FunctionCallExprSyntax.self), + let calledDeclReference = funcCallExpr.calledExpression.as(DeclReferenceExprSyntax.self), + calledDeclReference.baseName.text == "canImport", + let moduleNameSyntax = funcCallExpr.arguments.first?.expression.as(DeclReferenceExprSyntax.self) { + moduleNameSyntax.baseName.text + } else { + nil + } + + let clauseModules = importedModuleNames.map { + ImportedSwiftModule(name: $0, + availableWithModuleName: importModuleName, + alternativeModuleNames: []) + } + + // Assume single import from #else statement is fallback and use it as main source of symbols + if + clauseModules.count == 1 && + index == (ifConfigDecl.clauses.count - 1) && + clause.poundKeyword.tokenKind == .poundElse { + var fallbackModule: ImportedSwiftModule = clauseModules[0] + var moduleNames: [String] = [] + moduleNames.reserveCapacity(modules.count) + + for i in 0.. + var isMainSourceOfSymbols: Bool + + init(name: String, availableWithModuleName: String? = nil, alternativeModuleNames: Set = [], isMainSourceOfSymbols: Bool = false) { + self.name = name + self.availableWithModuleName = availableWithModuleName + self.alternativeModuleNames = alternativeModuleNames + self.isMainSourceOfSymbols = isMainSourceOfSymbols + } + + static func ==(lhs: Self, rhs: Self) -> Bool { + lhs.name == rhs.name + } + + func hash(into hasher: inout Hasher) { + hasher.combine(name) + } +} \ No newline at end of file diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftEffectSpecifier.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftEffectSpecifier.swift new file mode 100644 index 000000000..fb28586b1 --- /dev/null +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftEffectSpecifier.swift @@ -0,0 +1,18 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +enum SwiftEffectSpecifier: Equatable { + case `throws` + case `async` +} diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftEnumCaseParameter.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftEnumCaseParameter.swift new file mode 100644 index 000000000..55682152d --- /dev/null +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftEnumCaseParameter.swift @@ -0,0 +1,32 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftSyntax + +struct SwiftEnumCaseParameter: Equatable { + var name: String? + var type: SwiftType +} + +extension SwiftEnumCaseParameter { + init( + _ node: EnumCaseParameterSyntax, + lookupContext: SwiftTypeLookupContext + ) throws { + self.init( + name: node.firstName?.identifier?.name, + type: try SwiftType(node.type, lookupContext: lookupContext) + ) + } +} diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionSignature.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionSignature.swift index cd4dbf7aa..804769e59 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionSignature.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionSignature.swift @@ -15,17 +15,43 @@ import SwiftSyntax import SwiftSyntaxBuilder +enum SwiftGenericRequirement: Equatable { + case inherits(SwiftType, SwiftType) + case equals(SwiftType, SwiftType) +} + /// Provides a complete signature for a Swift function, which includes its /// parameters and return type. public struct SwiftFunctionSignature: Equatable { var selfParameter: SwiftSelfParameter? var parameters: [SwiftParameter] var result: SwiftResult + var effectSpecifiers: [SwiftEffectSpecifier] + var genericParameters: [SwiftGenericParameterDeclaration] + var genericRequirements: [SwiftGenericRequirement] + + var isAsync: Bool { + effectSpecifiers.contains(.async) + } - init(selfParameter: SwiftSelfParameter? = nil, parameters: [SwiftParameter], result: SwiftResult) { + var isThrowing: Bool { + effectSpecifiers.contains(.throws) + } + + init( + selfParameter: SwiftSelfParameter? = nil, + parameters: [SwiftParameter], + result: SwiftResult, + effectSpecifiers: [SwiftEffectSpecifier], + genericParameters: [SwiftGenericParameterDeclaration], + genericRequirements: [SwiftGenericRequirement] + ) { self.selfParameter = selfParameter self.parameters = parameters self.result = result + self.effectSpecifiers = effectSpecifiers + self.genericParameters = genericParameters + self.genericRequirements = genericRequirements } } @@ -47,46 +73,63 @@ extension SwiftFunctionSignature { init( _ node: InitializerDeclSyntax, enclosingType: SwiftType?, - symbolTable: SwiftSymbolTable + lookupContext: SwiftTypeLookupContext ) throws { - // Prohibit generics for now. - if let generics = node.genericParameterClause { - throw SwiftFunctionTranslationError.generic(generics) - } - guard let enclosingType else { throw SwiftFunctionTranslationError.missingEnclosingType(node) } - // We do not yet support failable initializers. - if node.optionalMark != nil { - throw SwiftFunctionTranslationError.failableInitializer(node) - } + let (genericParams, genericRequirements) = try Self.translateGenericParameters( + parameterClause: node.genericParameterClause, + whereClause: node.genericWhereClause, + lookupContext: lookupContext + ) + let (parameters, effectSpecifiers) = try Self.translateFunctionSignature( + node.signature, + lookupContext: lookupContext + ) + + let type = node.optionalMark != nil ? .optional(enclosingType) : enclosingType + + self.init( + selfParameter: .initializer(enclosingType), + parameters: parameters, + result: SwiftResult(convention: .direct, type: type), + effectSpecifiers: effectSpecifiers, + genericParameters: genericParams, + genericRequirements: genericRequirements + ) + } - // Prohibit generics for now. - if let generics = node.genericParameterClause { - throw SwiftFunctionTranslationError.generic(generics) + init( + _ node: EnumCaseElementSyntax, + enclosingType: SwiftType, + lookupContext: SwiftTypeLookupContext + ) throws { + let parameters = try node.parameterClause?.parameters.map { param in + try SwiftParameter(param, lookupContext: lookupContext) } self.init( selfParameter: .initializer(enclosingType), - parameters: try Self.translateFunctionSignature( - node.signature, - symbolTable: symbolTable - ), - result: SwiftResult(convention: .direct, type: enclosingType) + parameters: parameters ?? [], + result: SwiftResult(convention: .direct, type: enclosingType), + effectSpecifiers: [], + genericParameters: [], + genericRequirements: [] ) } init( _ node: FunctionDeclSyntax, enclosingType: SwiftType?, - symbolTable: SwiftSymbolTable + lookupContext: SwiftTypeLookupContext ) throws { - // Prohibit generics for now. - if let generics = node.genericParameterClause { - throw SwiftFunctionTranslationError.generic(generics) - } + let (genericParams, genericRequirements) = try Self.translateGenericParameters( + parameterClause: node.genericParameterClause, + whereClause: node.genericWhereClause, + lookupContext: lookupContext + ) // If this is a member of a type, so we will have a self parameter. Figure out the // type and convention for the self parameter. @@ -120,9 +163,9 @@ extension SwiftFunctionSignature { } // Translate the parameters. - let parameters = try Self.translateFunctionSignature( + let (parameters, effectSpecifiers) = try Self.translateFunctionSignature( node.signature, - symbolTable: symbolTable + lookupContext: lookupContext ) // Translate the result type. @@ -130,41 +173,223 @@ extension SwiftFunctionSignature { if let resultType = node.signature.returnClause?.type { result = try SwiftResult( convention: .direct, - type: SwiftType(resultType, symbolTable: symbolTable) + type: SwiftType(resultType, lookupContext: lookupContext) ) } else { result = .void } - self.init(selfParameter: selfParameter, parameters: parameters, result: result) + self.init( + selfParameter: selfParameter, + parameters: parameters, + result: result, + effectSpecifiers: effectSpecifiers, + genericParameters: genericParams, + genericRequirements: genericRequirements + ) + } + + static func translateGenericParameters( + parameterClause: GenericParameterClauseSyntax?, + whereClause: GenericWhereClauseSyntax?, + lookupContext: SwiftTypeLookupContext + ) throws -> (parameters: [SwiftGenericParameterDeclaration], requirements: [SwiftGenericRequirement]) { + var params: [SwiftGenericParameterDeclaration] = [] + var requirements: [SwiftGenericRequirement] = [] + + // Parameter clause + if let parameterClause { + for parameterNode in parameterClause.parameters { + guard parameterNode.specifier == nil else { + throw SwiftFunctionTranslationError.genericParameterSpecifier(parameterNode) + } + let param = try lookupContext.typeDeclaration(for: parameterNode, sourceFilePath: "FIXME_HAS_NO_PATH.swift") as! SwiftGenericParameterDeclaration + params.append(param) + if let inheritedNode = parameterNode.inheritedType { + let inherited = try SwiftType(inheritedNode, lookupContext: lookupContext) + requirements.append(.inherits(.genericParameter(param), inherited)) + } + } + } + + // Where clause + if let whereClause { + for requirementNode in whereClause.requirements { + let requirement: SwiftGenericRequirement + switch requirementNode.requirement { + case .conformanceRequirement(let conformance): + requirement = .inherits( + try SwiftType(conformance.leftType, lookupContext: lookupContext), + try SwiftType(conformance.rightType, lookupContext: lookupContext) + ) + case .sameTypeRequirement(let sameType): + guard let leftType = sameType.leftType.as(TypeSyntax.self) else { + throw SwiftFunctionTranslationError.expressionInGenericRequirement(requirementNode) + } + guard let rightType = sameType.rightType.as(TypeSyntax.self) else { + throw SwiftFunctionTranslationError.expressionInGenericRequirement(requirementNode) + } + requirement = .equals( + try SwiftType(leftType, lookupContext: lookupContext), + try SwiftType(rightType, lookupContext: lookupContext) + ) + case .layoutRequirement: + throw SwiftFunctionTranslationError.layoutRequirement(requirementNode) + } + requirements.append(requirement) + } + } + + return (params, requirements) } /// Translate the function signature, returning the list of translated - /// parameters. + /// parameters and effect specifiers. static func translateFunctionSignature( _ signature: FunctionSignatureSyntax, - symbolTable: SwiftSymbolTable - ) throws -> [SwiftParameter] { - // FIXME: Prohibit effects for now. - if let throwsClause = signature.effectSpecifiers?.throwsClause { - throw SwiftFunctionTranslationError.throws(throwsClause) + lookupContext: SwiftTypeLookupContext + ) throws -> ([SwiftParameter], [SwiftEffectSpecifier]) { + var effectSpecifiers = [SwiftEffectSpecifier]() + if signature.effectSpecifiers?.throwsClause != nil { + effectSpecifiers.append(.throws) } - if let asyncSpecifier = signature.effectSpecifiers?.asyncSpecifier { - throw SwiftFunctionTranslationError.async(asyncSpecifier) + if signature.effectSpecifiers?.asyncSpecifier != nil { + effectSpecifiers.append(.async) } - return try signature.parameterClause.parameters.map { param in - try SwiftParameter(param, symbolTable: symbolTable) + let parameters = try signature.parameterClause.parameters.map { param in + try SwiftParameter(param, lookupContext: lookupContext) } + + return (parameters, effectSpecifiers) } - init(_ varNode: VariableDeclSyntax, isSet: Bool, enclosingType: SwiftType?, symbolTable: SwiftSymbolTable) throws { + init( + _ varNode: VariableDeclSyntax, + isSet: Bool, + enclosingType: SwiftType?, + lookupContext: SwiftTypeLookupContext + ) throws { + + self.selfParameter = try Self.variableSelfParameter( + for: DeclSyntax(varNode), + enclosingType: enclosingType, + isSet: isSet) + + guard let binding = varNode.bindings.first, varNode.bindings.count == 1 else { + throw SwiftFunctionTranslationError.multipleBindings(varNode) + } + + guard let varTypeNode = binding.typeAnnotation?.type else { + throw SwiftFunctionTranslationError.missingTypeAnnotation(varNode) + } + let valueType = try SwiftType(varTypeNode, lookupContext: lookupContext) + + var effectSpecifiers: [SwiftEffectSpecifier]? = nil + switch binding.accessorBlock?.accessors { + case .getter(let getter): + if let getter = getter.as(AccessorDeclSyntax.self) { + effectSpecifiers = try Self.effectSpecifiers(from: getter) + } + case .accessors(let accessors): + if let getter = accessors.first(where: { $0.accessorSpecifier.tokenKind == .keyword(.get) }) { + effectSpecifiers = try Self.effectSpecifiers(from: getter) + } + default: + break + } + + self.effectSpecifiers = effectSpecifiers ?? [] + + if isSet { + self.parameters = [ + SwiftParameter(convention: .byValue, parameterName: "newValue", type: valueType) + ] + self.result = .void + } else { + self.parameters = [] + self.result = .init(convention: .direct, type: valueType) + } + self.genericParameters = [] + self.genericRequirements = [] + } + + init( + _ subscriptNode: SubscriptDeclSyntax, + isSet: Bool, + enclosingType: SwiftType?, + lookupContext: SwiftTypeLookupContext + ) throws { + self.selfParameter = try Self.variableSelfParameter( + for: DeclSyntax(subscriptNode), + enclosingType: enclosingType, + isSet: isSet) + + let valueType: SwiftType = try SwiftType(subscriptNode.returnClause.type, lookupContext: lookupContext) + var nodeParameters = try subscriptNode.parameterClause.parameters.map { param in + try SwiftParameter(param, lookupContext: lookupContext) + } + + var effectSpecifiers: [SwiftEffectSpecifier]? = nil + switch subscriptNode.accessorBlock?.accessors { + case .getter(let getter): + if let getter = getter.as(AccessorDeclSyntax.self) { + effectSpecifiers = try Self.effectSpecifiers(from: getter) + } + case .accessors(let accessors): + if let getter = accessors.first(where: { $0.accessorSpecifier.tokenKind == .keyword(.get) }) { + effectSpecifiers = try Self.effectSpecifiers(from: getter) + } + default: + break + } + + self.effectSpecifiers = effectSpecifiers ?? [] + + if isSet { + nodeParameters.append(SwiftParameter(convention: .byValue, parameterName: "newValue", type: valueType)) + self.result = .void + } else { + self.result = .init(convention: .direct, type: valueType) + } + + self.parameters = nodeParameters + self.genericParameters = [] + self.genericRequirements = [] + } + + private static func effectSpecifiers(from decl: AccessorDeclSyntax) throws -> [SwiftEffectSpecifier] { + var effectSpecifiers = [SwiftEffectSpecifier]() + if decl.effectSpecifiers?.throwsClause != nil { + effectSpecifiers.append(.throws) + } + if let asyncSpecifier = decl.effectSpecifiers?.asyncSpecifier { + effectSpecifiers.append(.async) + } + return effectSpecifiers + } + + private static func variableSelfParameter( + for decl: DeclSyntax, + enclosingType: SwiftType?, + isSet: Bool + ) throws -> SwiftSelfParameter? { + let modifiers: DeclModifierListSyntax? = + switch decl.as(DeclSyntaxEnum.self) { + case .variableDecl(let varDecl): varDecl.modifiers + case .subscriptDecl(let subscriptDecl): subscriptDecl.modifiers + default: nil + } + + guard let modifiers else { + return nil + } // If this is a member of a type, so we will have a self parameter. Figure out the // type and convention for the self parameter. if let enclosingType { var isStatic = false - for modifier in varNode.modifiers { + for modifier in modifiers { switch modifier.name.tokenKind { case .keyword(.static): isStatic = true case .keyword(.class): throw SwiftFunctionTranslationError.classMethod(modifier.name) @@ -173,9 +398,9 @@ extension SwiftFunctionSignature { } if isStatic { - self.selfParameter = .staticMethod(enclosingType) + return .staticMethod(enclosingType) } else { - self.selfParameter = .instance( + return .instance( SwiftParameter( convention: isSet && !enclosingType.isReferenceType ? .inout : .byValue, type: enclosingType @@ -183,29 +408,30 @@ extension SwiftFunctionSignature { ) } } else { - self.selfParameter = nil + return nil } + } +} - guard let binding = varNode.bindings.first, varNode.bindings.count == 1 else { - throw SwiftFunctionTranslationError.multipleBindings(varNode) +extension VariableDeclSyntax { + /// Determine what operations (i.e. get and/or set) supported in this `VariableDeclSyntax` + /// + /// - Parameters: + /// - binding the pattern binding in this declaration. + func supportedAccessorKinds(binding: PatternBindingSyntax) -> AccessorBlockSyntax.SupportedAccessorKinds { + if self.bindingSpecifier.tokenKind == .keyword(.let) { + return [.get] } - guard let varTypeNode = binding.typeAnnotation?.type else { - throw SwiftFunctionTranslationError.missingTypeAnnotation(varNode) + if let accessorBlock = binding.accessorBlock { + return accessorBlock.supportedAccessorKinds() } - let valueType = try SwiftType(varTypeNode, symbolTable: symbolTable) - if isSet { - self.parameters = [SwiftParameter(convention: .byValue, parameterName: "newValue", type: valueType)] - self.result = .void - } else { - self.parameters = [] - self.result = .init(convention: .direct, type: valueType) - } + return [.get, .set] } } -extension VariableDeclSyntax { +extension AccessorBlockSyntax { struct SupportedAccessorKinds: OptionSet { var rawValue: UInt8 @@ -213,17 +439,9 @@ extension VariableDeclSyntax { static var set: Self = .init(rawValue: 1 << 1) } - /// Determine what operations (i.e. get and/or set) supported in this `VariableDeclSyntax` - /// - /// - Parameters: - /// - binding the pattern binding in this declaration. - func supportedAccessorKinds(binding: PatternBindingSyntax) -> SupportedAccessorKinds { - if self.bindingSpecifier == .keyword(.let) { - return [.get] - } - - if let accessorBlock = binding.accessorBlock { - switch accessorBlock.accessors { + /// Determine what operations (i.e. get and/or set) supported in this `AccessorBlockSyntax` + func supportedAccessorKinds() -> SupportedAccessorKinds { + switch self.accessors { case .getter: return [.get] case .accessors(let accessors): @@ -239,20 +457,19 @@ extension VariableDeclSyntax { } return [.get] } - } - - return [.get, .set] } } enum SwiftFunctionTranslationError: Error { case `throws`(ThrowsClauseSyntax) case async(TokenSyntax) - case generic(GenericParameterClauseSyntax) case classMethod(TokenSyntax) case missingEnclosingType(InitializerDeclSyntax) case failableInitializer(InitializerDeclSyntax) case multipleBindings(VariableDeclSyntax) case missingTypeAnnotation(VariableDeclSyntax) case unsupportedAccessor(AccessorDeclSyntax) + case genericParameterSpecifier(GenericParameterSyntax) + case expressionInGenericRequirement(GenericRequirementSyntax) + case layoutRequirement(GenericRequirementSyntax) } diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionType.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionType.swift index 565a24c3e..da6fd2a2a 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionType.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionType.swift @@ -40,18 +40,18 @@ extension SwiftFunctionType { init( _ node: FunctionTypeSyntax, convention: Convention, - symbolTable: SwiftSymbolTable + lookupContext: SwiftTypeLookupContext ) throws { self.convention = convention self.parameters = try node.parameters.map { param in let isInout = param.inoutKeyword != nil return SwiftParameter( convention: isInout ? .inout : .byValue, - type: try SwiftType(param.type, symbolTable: symbolTable) + type: try SwiftType(param.type, lookupContext: lookupContext) ) } - self.resultType = try SwiftType(node.returnClause.type, symbolTable: symbolTable) + self.resultType = try SwiftType(node.returnClause.type, lookupContext: lookupContext) // check for effect specifiers if let throwsClause = node.effectSpecifiers?.throwsClause { diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownModules.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownModules.swift new file mode 100644 index 000000000..1be8071c4 --- /dev/null +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownModules.swift @@ -0,0 +1,122 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftSyntax +import SwiftSyntaxBuilder + +enum SwiftKnownModule: String { + case swift = "Swift" + case foundation = "Foundation" + case foundationEssentials = "FoundationEssentials" + + var name: String { + return self.rawValue + } + + var symbolTable: SwiftModuleSymbolTable { + return switch self { + case .swift: swiftSymbolTable + case .foundation: foundationSymbolTable + case .foundationEssentials: foundationEssentialsSymbolTable + } + } + + var sourceFile: SourceFileSyntax { + return switch self { + case .swift: swiftSourceFile + case .foundation: foundationEssentialsSourceFile + case .foundationEssentials: foundationEssentialsSourceFile + } + } +} + +private var swiftSymbolTable: SwiftModuleSymbolTable { + var builder = SwiftParsedModuleSymbolTableBuilder(moduleName: "Swift", importedModules: [:]) + builder.handle(sourceFile: swiftSourceFile, sourceFilePath: "SwiftStdlib.swift") // FIXME: missing path here + return builder.finalize() +} + +private var foundationEssentialsSymbolTable: SwiftModuleSymbolTable { + var builder = SwiftParsedModuleSymbolTableBuilder( + moduleName: "FoundationEssentials", + requiredAvailablityOfModuleWithName: "FoundationEssentials", + alternativeModules: .init(isMainSourceOfSymbols: false, moduleNames: ["Foundation"]), + importedModules: ["Swift": swiftSymbolTable]) + builder.handle(sourceFile: foundationEssentialsSourceFile, sourceFilePath: "FakeFoundation.swift") + return builder.finalize() +} + +private var foundationSymbolTable: SwiftModuleSymbolTable { + var builder = SwiftParsedModuleSymbolTableBuilder( + moduleName: "Foundation", + alternativeModules: .init(isMainSourceOfSymbols: true, moduleNames: ["FoundationEssentials"]), + importedModules: ["Swift": swiftSymbolTable]) + builder.handle(sourceFile: foundationSourceFile, sourceFilePath: "Foundation.swift") + return builder.finalize() +} + +private let swiftSourceFile: SourceFileSyntax = """ + public struct Bool {} + public struct Int {} + public struct UInt {} + public struct Int8 {} + public struct UInt8 {} + public struct Int16 {} + public struct UInt16 {} + public struct Int32 {} + public struct UInt32 {} + public struct Int64 {} + public struct UInt64 {} + public struct Float {} + public struct Double {} + + public struct UnsafeRawPointer {} + public struct UnsafeMutableRawPointer {} + public struct UnsafeRawBufferPointer {} + public struct UnsafeMutableRawBufferPointer {} + + public struct UnsafePointer {} + public struct UnsafeMutablePointer {} + + public struct UnsafeBufferPointer {} + public struct UnsafeMutableBufferPointer {} + + public struct Optional {} + + public struct Array {} + + // FIXME: Support 'typealias Void = ()' + public struct Void {} + + public struct String { + public init(cString: UnsafePointer) + public func withCString(_ body: (UnsafePointer) -> Void) + } + """ + +private let foundationEssentialsSourceFile: SourceFileSyntax = """ + public protocol DataProtocol {} + + public struct Data: DataProtocol { + public init(bytes: UnsafeRawPointer, count: Int) + public var count: Int { get } + public func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) -> Void) + } + """ + +private var foundationSourceFile: SourceFileSyntax { + // On platforms other than Darwin, imports such as FoundationEssentials, FoundationNetworking, etc. are used, + // so this file should be created by combining the files of the aforementioned modules. + foundationEssentialsSourceFile +} diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypeDecls.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypeDecls.swift new file mode 100644 index 000000000..363663217 --- /dev/null +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypeDecls.swift @@ -0,0 +1,82 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftSyntax + +enum SwiftKnownTypeDeclKind: String, Hashable { + // Swift + case bool = "Swift.Bool" + case int = "Swift.Int" + case uint = "Swift.UInt" + case int8 = "Swift.Int8" + case uint8 = "Swift.UInt8" + case int16 = "Swift.Int16" + case uint16 = "Swift.UInt16" + case int32 = "Swift.Int32" + case uint32 = "Swift.UInt32" + case int64 = "Swift.Int64" + case uint64 = "Swift.UInt64" + case float = "Swift.Float" + case double = "Swift.Double" + case unsafeRawPointer = "Swift.UnsafeRawPointer" + case unsafeMutableRawPointer = "Swift.UnsafeMutableRawPointer" + case unsafeRawBufferPointer = "Swift.UnsafeRawBufferPointer" + case unsafeMutableRawBufferPointer = "Swift.UnsafeMutableRawBufferPointer" + case unsafePointer = "Swift.UnsafePointer" + case unsafeMutablePointer = "Swift.UnsafeMutablePointer" + case unsafeBufferPointer = "Swift.UnsafeBufferPointer" + case unsafeMutableBufferPointer = "Swift.UnsafeMutableBufferPointer" + case optional = "Swift.Optional" + case void = "Swift.Void" + case string = "Swift.String" + case array = "Swift.Array" + + // Foundation + case foundationDataProtocol = "Foundation.DataProtocol" + case essentialsDataProtocol = "FoundationEssentials.DataProtocol" + case foundationData = "Foundation.Data" + case essentialsData = "FoundationEssentials.Data" + + var moduleAndName: (module: String, name: String) { + let qualified = self.rawValue + let period = qualified.firstIndex(of: ".")! + return ( + module: String(qualified[.. SwiftType { + .nominal( + SwiftNominalType( + nominalTypeDecl: symbolTable[.unsafePointer], + genericArguments: [pointeeType] + ) + ) + } + + func unsafeMutablePointer(_ pointeeType: SwiftType) -> SwiftType { + .nominal( + SwiftNominalType( + nominalTypeDecl: symbolTable[.unsafeMutablePointer], + genericArguments: [pointeeType] + ) + ) + } + + func unsafeBufferPointer(_ elementType: SwiftType) -> SwiftType { + .nominal( + SwiftNominalType( + nominalTypeDecl: symbolTable[.unsafeBufferPointer], + genericArguments: [elementType] + ) + ) + } + + func unsafeMutableBufferPointer(_ elementType: SwiftType) -> SwiftType { + .nominal( + SwiftNominalType( + nominalTypeDecl: symbolTable[.unsafeMutableBufferPointer], + genericArguments: [elementType] + ) + ) + } + + /// Returns the known representative concrete type if there is one for the + /// given protocol kind. E.g. `String` for `StringProtocol` + func representativeType(of knownProtocol: SwiftKnownTypeDeclKind) -> SwiftType? { + switch knownProtocol { + case .foundationDataProtocol: return self.foundationData + case .essentialsDataProtocol: return self.essentialsData + default: return nil + } + } +} diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftModuleSymbolTable.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftModuleSymbolTable.swift index f1bbaa12a..2db19928e 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftModuleSymbolTable.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftModuleSymbolTable.swift @@ -19,6 +19,12 @@ struct SwiftModuleSymbolTable: SwiftSymbolTableProtocol { /// The name of this module. let moduleName: String + /// The name of module required to be imported and checked via canImport statement. + let requiredAvailablityOfModuleWithName: String? + + /// Data about alternative modules which provides desired symbos e.g. FoundationEssentials is non-Darwin platform alternative for Foundation + let alternativeModules: AlternativeModuleNamesData? + /// The top-level nominal types, found by name. var topLevelTypes: [String: SwiftNominalTypeDeclaration] = [:] @@ -36,4 +42,18 @@ struct SwiftModuleSymbolTable: SwiftSymbolTableProtocol { func lookupNestedType(_ name: String, parent: SwiftNominalTypeDeclaration) -> SwiftNominalTypeDeclaration? { nestedTypes[parent]?[name] } + + func isAlternative(for moduleName: String) -> Bool { + alternativeModules.flatMap { $0.moduleNames.contains(moduleName) } ?? false + } +} + +extension SwiftModuleSymbolTable { + struct AlternativeModuleNamesData { + /// Flag indicating module should be used as source of symbols to avoid duplication of symbols. + let isMainSourceOfSymbols: Bool + + /// Names of modules which are alternative for currently checked module. + let moduleNames: Set + } } diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftNominalTypeDeclaration.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftNominalTypeDeclaration.swift index c83301266..0b8b5651a 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftNominalTypeDeclaration.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftNominalTypeDeclaration.swift @@ -18,9 +18,41 @@ import SwiftSyntax @_spi(Testing) public typealias NominalTypeDeclSyntaxNode = any DeclGroupSyntax & NamedDeclSyntax & WithAttributesSyntax & WithModifiersSyntax +package class SwiftTypeDeclaration { + + // The short path from module root to the file in which this nominal was originally declared. + // E.g. for `Sources/Example/My/Types.swift` it would be `My/Types.swift`. + let sourceFilePath: String + + /// The module in which this nominal type is defined. If this is a nested type, the + /// module might be different from that of the parent type, if this nominal type + /// is defined in an extension within another module. + let moduleName: String + + /// The name of this nominal type, e.g. 'MyCollection'. + let name: String + + init(sourceFilePath: String, moduleName: String, name: String) { + self.sourceFilePath = sourceFilePath + self.moduleName = moduleName + self.name = name + } +} + +/// A syntax node paired with a simple file path +package struct SwiftJavaInputFile { + let syntax: SourceFileSyntax + /// Simple file path of the file from which the syntax node was parsed. + let path: String + package init(syntax: SourceFileSyntax, path: String) { + self.syntax = syntax + self.path = path + } +} + /// Describes a nominal type declaration, which can be of any kind (class, struct, etc.) /// and has a name, parent type (if nested), and owning module. -package class SwiftNominalTypeDeclaration { +package class SwiftNominalTypeDeclaration: SwiftTypeDeclaration { enum Kind { case actor case `class` @@ -31,41 +63,33 @@ package class SwiftNominalTypeDeclaration { /// The syntax node this declaration is derived from. /// Can be `nil` if this is loaded from a .swiftmodule. - var syntax: NominalTypeDeclSyntaxNode? + let syntax: NominalTypeDeclSyntaxNode? /// The kind of nominal type. - var kind: Kind + let kind: Kind /// The parent nominal type when this nominal type is nested inside another type, e.g., /// MyCollection.Iterator. - var parent: SwiftNominalTypeDeclaration? - - /// The module in which this nominal type is defined. If this is a nested type, the - /// module might be different from that of the parent type, if this nominal type - /// is defined in an extension within another module. - var moduleName: String - - /// The name of this nominal type, e.g., 'MyCollection'. - var name: String + let parent: SwiftNominalTypeDeclaration? // TODO: Generic parameters. /// Identify this nominal declaration as one of the known standard library /// types, like 'Swift.Int[. - lazy var knownStandardLibraryType: KnownStandardLibraryType? = { + lazy var knownTypeKind: SwiftKnownTypeDeclKind? = { self.computeKnownStandardLibraryType() }() /// Create a nominal type declaration from the syntax node for a nominal type /// declaration. init( + sourceFilePath: String, moduleName: String, parent: SwiftNominalTypeDeclaration?, node: NominalTypeDeclSyntaxNode ) { - self.moduleName = moduleName self.parent = parent - self.name = node.name.text + self.syntax = node // Determine the kind from the syntax node. switch Syntax(node).as(SyntaxEnum.self) { @@ -76,16 +100,45 @@ package class SwiftNominalTypeDeclaration { case .structDecl: self.kind = .struct default: fatalError("Not a nominal type declaration") } + super.init(sourceFilePath: sourceFilePath, moduleName: moduleName, name: node.name.text) } + lazy var firstInheritanceType: TypeSyntax? = { + guard let firstInheritanceType = self.syntax?.inheritanceClause?.inheritedTypes.first else { + return nil + } + + return firstInheritanceType.type + }() + + var inheritanceTypes: InheritedTypeListSyntax? { + self.syntax?.inheritanceClause?.inheritedTypes + } + + /// Returns true if this type conforms to `Sendable` and therefore is "threadsafe". + lazy var isSendable: Bool = { + // Check if Sendable is in the inheritance list + guard let inheritanceClause = self.syntax?.inheritanceClause else { + return false + } + + for inheritedType in inheritanceClause.inheritedTypes { + if inheritedType.type.trimmedDescription == "Sendable" { + return true + } + } + + return false + }() + /// Determine the known standard library type for this nominal type /// declaration. - private func computeKnownStandardLibraryType() -> KnownStandardLibraryType? { - if parent != nil || moduleName != "Swift" { + private func computeKnownStandardLibraryType() -> SwiftKnownTypeDeclKind? { + if parent != nil { return nil } - return KnownStandardLibraryType(typeNameInSwiftModule: name) + return SwiftKnownTypeDeclKind(rawValue: "\(moduleName).\(name)") } package var qualifiedName: String { @@ -106,13 +159,26 @@ package class SwiftNominalTypeDeclaration { } } -extension SwiftNominalTypeDeclaration: Equatable { - package static func ==(lhs: SwiftNominalTypeDeclaration, rhs: SwiftNominalTypeDeclaration) -> Bool { +package class SwiftGenericParameterDeclaration: SwiftTypeDeclaration { + let syntax: GenericParameterSyntax + + init( + sourceFilePath: String, + moduleName: String, + node: GenericParameterSyntax + ) { + self.syntax = node + super.init(sourceFilePath: sourceFilePath, moduleName: moduleName, name: node.name.text) + } +} + +extension SwiftTypeDeclaration: Equatable { + package static func ==(lhs: SwiftTypeDeclaration, rhs: SwiftTypeDeclaration) -> Bool { lhs === rhs } } -extension SwiftNominalTypeDeclaration: Hashable { +extension SwiftTypeDeclaration: Hashable { package func hash(into hasher: inout Hasher) { hasher.combine(ObjectIdentifier(self)) } diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftParameter.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftParameter.swift index 24b3d8b49..63f7d75bc 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftParameter.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftParameter.swift @@ -58,7 +58,17 @@ enum SwiftParameterConvention: Equatable { } extension SwiftParameter { - init(_ node: FunctionParameterSyntax, symbolTable: SwiftSymbolTable) throws { + init(_ node: EnumCaseParameterSyntax, lookupContext: SwiftTypeLookupContext) throws { + self.convention = .byValue + self.type = try SwiftType(node.type, lookupContext: lookupContext) + self.argumentLabel = nil + self.parameterName = node.firstName?.identifier?.name + self.argumentLabel = node.firstName?.identifier?.name + } +} + +extension SwiftParameter { + init(_ node: FunctionParameterSyntax, lookupContext: SwiftTypeLookupContext) throws { // Determine the convention. The default is by-value, but there are // specifiers on the type for other conventions (like `inout`). var type = node.type @@ -90,7 +100,7 @@ extension SwiftParameter { self.convention = convention // Determine the type. - self.type = try SwiftType(type, symbolTable: symbolTable) + self.type = try SwiftType(type, lookupContext: lookupContext) // FIXME: swift-syntax itself should have these utilities based on identifiers. if let secondName = node.secondName { diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftParsedModuleSymbolTable.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftParsedModuleSymbolTable.swift deleted file mode 100644 index 1eb37eac3..000000000 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftParsedModuleSymbolTable.swift +++ /dev/null @@ -1,120 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift.org project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift.org project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import SwiftSyntax - -struct SwiftParsedModuleSymbolTable { - var symbolTable: SwiftModuleSymbolTable - - /// The nominal type declarations, indexed by the nominal type declaration syntax node. - var nominalTypeDeclarations: [SyntaxIdentifier: SwiftNominalTypeDeclaration] = [:] - - /// Mapping from the nominal type declarations in this module back to the syntax - /// node. This is the reverse mapping of 'nominalTypeDeclarations'. - var nominalTypeSyntaxNodes: [SwiftNominalTypeDeclaration: NominalTypeDeclSyntaxNode] = [:] - - init(moduleName: String) { - symbolTable = .init(moduleName: moduleName) - } -} - -extension SwiftParsedModuleSymbolTable: SwiftSymbolTableProtocol { - var moduleName: String { - symbolTable.moduleName - } - - func lookupTopLevelNominalType(_ name: String) -> SwiftNominalTypeDeclaration? { - symbolTable.lookupTopLevelNominalType(name) - } - - func lookupNestedType(_ name: String, parent: SwiftNominalTypeDeclaration) -> SwiftNominalTypeDeclaration? { - symbolTable.lookupNestedType(name, parent: parent) - } -} - -extension SwiftParsedModuleSymbolTable { - /// Look up a nominal type declaration based on its syntax node. - func lookup(_ node: NominalTypeDeclSyntaxNode) -> SwiftNominalTypeDeclaration? { - nominalTypeDeclarations[node.id] - } - - /// Add a nominal type declaration and all of the nested types within it to the symbol - /// table. - @discardableResult - mutating func addNominalTypeDeclaration( - _ node: NominalTypeDeclSyntaxNode, - parent: SwiftNominalTypeDeclaration? - ) -> SwiftNominalTypeDeclaration { - // If we have already recorded this nominal type declaration, we're done. - if let existingNominal = nominalTypeDeclarations[node.id] { - return existingNominal - } - - // Otherwise, create the nominal type declaration. - let nominalTypeDecl = SwiftNominalTypeDeclaration( - moduleName: moduleName, - parent: parent, - node: node - ) - - // Ensure that we can find this nominal type declaration again based on the syntax - // node, and vice versa. - nominalTypeDeclarations[node.id] = nominalTypeDecl - nominalTypeSyntaxNodes[nominalTypeDecl] = node - - if let parent { - // For nested types, make them discoverable from the parent type. - symbolTable.nestedTypes[parent, default: [:]][node.name.text] = nominalTypeDecl - } else { - // For top-level types, make them discoverable by name. - symbolTable.topLevelTypes[node.name.text] = nominalTypeDecl - } - - // Find any nested types within this nominal type and add them. - for member in node.memberBlock.members { - if let nominalMember = member.decl.asNominal { - addNominalTypeDeclaration(nominalMember, parent: nominalTypeDecl) - } - } - - return nominalTypeDecl - } - - /// Add any nested types within the given extension (with known extended nominal type - /// declaration) to the symbol table. - mutating func addExtension( - _ extensionNode: ExtensionDeclSyntax, - extending nominalTypeDecl: SwiftNominalTypeDeclaration - ) { - // Find any nested types within this extension and add them. - for member in extensionNode.memberBlock.members { - if let nominalMember = member.decl.asNominal { - addNominalTypeDeclaration(nominalMember, parent: nominalTypeDecl) - } - } - } -} - -extension DeclSyntaxProtocol { - var asNominal: NominalTypeDeclSyntaxNode? { - switch DeclSyntax(self).as(DeclSyntaxEnum.self) { - case .actorDecl(let actorDecl): actorDecl - case .classDecl(let classDecl): classDecl - case .enumDecl(let enumDecl): enumDecl - case .protocolDecl(let protocolDecl): protocolDecl - case .structDecl(let structDecl): structDecl - default: nil - } - } -} diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftParsedModuleSymbolTableBuilder.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftParsedModuleSymbolTableBuilder.swift new file mode 100644 index 000000000..c55864102 --- /dev/null +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftParsedModuleSymbolTableBuilder.swift @@ -0,0 +1,194 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftSyntax + +struct SwiftParsedModuleSymbolTableBuilder { + let log: Logger? + + /// The symbol table being built. + var symbolTable: SwiftModuleSymbolTable + + /// Imported modules to resolve type syntax. + let importedModules: [String: SwiftModuleSymbolTable] + + /// Extension decls their extended type hasn't been resolved. + var unresolvedExtensions: [ExtensionDeclSyntax] + + init( + moduleName: String, + requiredAvailablityOfModuleWithName: String? = nil, + alternativeModules: SwiftModuleSymbolTable.AlternativeModuleNamesData? = nil, + importedModules: [String: SwiftModuleSymbolTable], + log: Logger? = nil + ) { + self.log = log + self.symbolTable = .init( + moduleName: moduleName, + requiredAvailablityOfModuleWithName: requiredAvailablityOfModuleWithName, + alternativeModules: alternativeModules) + self.importedModules = importedModules + self.unresolvedExtensions = [] + } + + var moduleName: String { + symbolTable.moduleName + } +} + +extension SwiftParsedModuleSymbolTableBuilder { + + mutating func handle( + sourceFile: SourceFileSyntax, + sourceFilePath: String + ) { + // Find top-level type declarations. + for statement in sourceFile.statements { + // We only care about declarations. + guard case .decl(let decl) = statement.item else { + continue + } + + if let nominalTypeNode = decl.asNominal { + self.handle(sourceFilePath: sourceFilePath, nominalTypeDecl: nominalTypeNode, parent: nil) + } + if let extensionNode = decl.as(ExtensionDeclSyntax.self) { + self.handle(extensionDecl: extensionNode, sourceFilePath: sourceFilePath) + } + } + } + + /// Add a nominal type declaration and all of the nested types within it to the symbol + /// table. + mutating func handle( + sourceFilePath: String, + nominalTypeDecl node: NominalTypeDeclSyntaxNode, + parent: SwiftNominalTypeDeclaration? + ) { + // If we have already recorded a nominal type with the name in this module, + // it's an invalid redeclaration. + if let _ = symbolTable.lookupType(node.name.text, parent: parent) { + log?.debug("Failed to add a decl into symbol table: redeclaration; " + node.nameForDebug) + return + } + + // Otherwise, create the nominal type declaration. + let nominalTypeDecl = SwiftNominalTypeDeclaration( + sourceFilePath: sourceFilePath, + moduleName: moduleName, + parent: parent, + node: node + ) + + if let parent { + // For nested types, make them discoverable from the parent type. + symbolTable.nestedTypes[parent, default: [:]][nominalTypeDecl.name] = nominalTypeDecl + } else { + // For top-level types, make them discoverable by name. + symbolTable.topLevelTypes[nominalTypeDecl.name] = nominalTypeDecl + } + + self.handle(sourceFilePath: sourceFilePath, memberBlock: node.memberBlock, parent: nominalTypeDecl) + } + + mutating func handle( + sourceFilePath: String, + memberBlock node: MemberBlockSyntax, + parent: SwiftNominalTypeDeclaration + ) { + for member in node.members { + // Find any nested types within this nominal type and add them. + if let nominalMember = member.decl.asNominal { + self.handle(sourceFilePath: sourceFilePath, nominalTypeDecl: nominalMember, parent: parent) + } + } + + } + + mutating func handle( + extensionDecl node: ExtensionDeclSyntax, + sourceFilePath: String + ) { + if !self.tryHandle(extension: node, sourceFilePath: sourceFilePath) { + self.unresolvedExtensions.append(node) + } + } + + /// Add any nested types within the given extension to the symbol table. + /// If the extended nominal type can't be resolved, returns false. + mutating func tryHandle( + extension node: ExtensionDeclSyntax, + sourceFilePath: String + ) -> Bool { + // Try to resolve the type referenced by this extension declaration. + // If it fails, we'll try again later. + let table = SwiftSymbolTable( + parsedModule: symbolTable, + importedModules: importedModules + ) + let lookupContext = SwiftTypeLookupContext(symbolTable: table) + guard let extendedType = try? SwiftType(node.extendedType, lookupContext: lookupContext) else { + return false + } + guard let extendedNominal = extendedType.asNominalTypeDeclaration else { + // Extending type was not a nominal type. Ignore it. + return true + } + + // Find any nested types within this extension and add them. + self.handle(sourceFilePath: sourceFilePath, memberBlock: node.memberBlock, parent: extendedNominal) + return true + } + + /// Finalize the symbol table and return it. + mutating func finalize() -> SwiftModuleSymbolTable { + // Handle the unresolved extensions. + // The work queue is required because, the extending type might be declared + // in another extension that hasn't been processed. E.g.: + // + // extension Outer.Inner { struct Deeper {} } + // extension Outer { struct Inner {} } + // struct Outer {} + // + while !unresolvedExtensions.isEmpty { + var extensions = self.unresolvedExtensions + extensions.removeAll(where: { + self.tryHandle(extension: $0, sourceFilePath: "FIXME_MISSING_FILEPATH.swift") // FIXME: missing filepath here in finalize + }) + + // If we didn't resolve anything, we're done. + if extensions.count == unresolvedExtensions.count { + break + } + + assert(extensions.count < unresolvedExtensions.count) + self.unresolvedExtensions = extensions + } + + return symbolTable + } +} + +extension DeclSyntaxProtocol { + var asNominal: NominalTypeDeclSyntaxNode? { + switch DeclSyntax(self).as(DeclSyntaxEnum.self) { + case .actorDecl(let actorDecl): actorDecl + case .classDecl(let classDecl): classDecl + case .enumDecl(let enumDecl): enumDecl + case .protocolDecl(let protocolDecl): protocolDecl + case .structDecl(let structDecl): structDecl + default: nil + } + } +} diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftStandardLibraryTypes.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftStandardLibraryTypes.swift deleted file mode 100644 index 4b07c5751..000000000 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftStandardLibraryTypes.swift +++ /dev/null @@ -1,186 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift.org project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift.org project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import SwiftSyntax - -enum KnownStandardLibraryType: String, Hashable, CaseIterable { - case bool = "Bool" - case int = "Int" - case uint = "UInt" - case int8 = "Int8" - case uint8 = "UInt8" - case int16 = "Int16" - case uint16 = "UInt16" - case int32 = "Int32" - case uint32 = "UInt32" - case int64 = "Int64" - case uint64 = "UInt64" - case float = "Float" - case double = "Double" - case unsafeRawPointer = "UnsafeRawPointer" - case unsafeMutableRawPointer = "UnsafeMutableRawPointer" - case unsafePointer = "UnsafePointer" - case unsafeMutablePointer = "UnsafeMutablePointer" - case unsafeBufferPointer = "UnsafeBufferPointer" - case unsafeMutableBufferPointer = "UnsafeMutableBufferPointer" - case string = "String" - case void = "Void" - - var typeName: String { rawValue } - - init?(typeNameInSwiftModule: String) { - self.init(rawValue: typeNameInSwiftModule) - } - - /// Whether this declaration is generic. - var isGeneric: Bool { - switch self { - case .bool, .double, .float, .int, .int8, .int16, .int32, .int64, - .uint, .uint8, .uint16, .uint32, .uint64, .unsafeRawPointer, - .unsafeMutableRawPointer, .string, .void: - false - - case .unsafePointer, .unsafeMutablePointer, .unsafeBufferPointer, - .unsafeMutableBufferPointer: - true - } - } -} - -/// Captures many types from the Swift standard library in their most basic -/// forms, so that the translator can reason about them in source code. -public struct SwiftStandardLibraryTypes { - // Swift.UnsafePointer - let unsafePointerDecl: SwiftNominalTypeDeclaration - - // Swift.UnsafeMutablePointer - let unsafeMutablePointerDecl: SwiftNominalTypeDeclaration - - // Swift.UnsafeBufferPointer - let unsafeBufferPointerDecl: SwiftNominalTypeDeclaration - - // Swift.UnsafeMutableBufferPointer - let unsafeMutableBufferPointerDecl: SwiftNominalTypeDeclaration - - /// Mapping from known standard library types to their nominal type declaration. - let knownTypeToNominal: [KnownStandardLibraryType: SwiftNominalTypeDeclaration] - - /// Mapping from nominal type declarations to known types. - let nominalTypeDeclToKnownType: [SwiftNominalTypeDeclaration: KnownStandardLibraryType] - - private static func recordKnownType( - _ type: KnownStandardLibraryType, - _ syntax: NominalTypeDeclSyntaxNode, - knownTypeToNominal: inout [KnownStandardLibraryType: SwiftNominalTypeDeclaration], - nominalTypeDeclToKnownType: inout [SwiftNominalTypeDeclaration: KnownStandardLibraryType], - parsedModule: inout SwiftParsedModuleSymbolTable - ) { - let nominalDecl = parsedModule.addNominalTypeDeclaration(syntax, parent: nil) - knownTypeToNominal[type] = nominalDecl - nominalTypeDeclToKnownType[nominalDecl] = type - } - - private static func recordKnownNonGenericStruct( - _ type: KnownStandardLibraryType, - knownTypeToNominal: inout [KnownStandardLibraryType: SwiftNominalTypeDeclaration], - nominalTypeDeclToKnownType: inout [SwiftNominalTypeDeclaration: KnownStandardLibraryType], - parsedModule: inout SwiftParsedModuleSymbolTable - ) { - recordKnownType( - type, - StructDeclSyntax( - name: .identifier(type.typeName), - memberBlock: .init(members: []) - ), - knownTypeToNominal: &knownTypeToNominal, - nominalTypeDeclToKnownType: &nominalTypeDeclToKnownType, - parsedModule: &parsedModule - ) - } - - init(into parsedModule: inout SwiftParsedModuleSymbolTable) { - // Pointer types - self.unsafePointerDecl = parsedModule.addNominalTypeDeclaration( - StructDeclSyntax( - name: .identifier("UnsafePointer"), - genericParameterClause: .init( - parameters: [GenericParameterSyntax(name: .identifier("Element"))] - ), - memberBlock: .init(members: []) - ), - parent: nil - ) - - self.unsafeMutablePointerDecl = parsedModule.addNominalTypeDeclaration( - StructDeclSyntax( - name: .identifier("UnsafeMutablePointer"), - genericParameterClause: .init( - parameters: [GenericParameterSyntax(name: .identifier("Element"))] - ), - memberBlock: .init(members: []) - ), - parent: nil - ) - - self.unsafeBufferPointerDecl = parsedModule.addNominalTypeDeclaration( - StructDeclSyntax( - name: .identifier("UnsafeBufferPointer"), - genericParameterClause: .init( - parameters: [GenericParameterSyntax(name: .identifier("Element"))] - ), - memberBlock: .init(members: []) - ), - parent: nil - ) - - self.unsafeMutableBufferPointerDecl = parsedModule.addNominalTypeDeclaration( - StructDeclSyntax( - name: .identifier("UnsafeMutableBufferPointer"), - genericParameterClause: .init( - parameters: [GenericParameterSyntax(name: .identifier("Element"))] - ), - memberBlock: .init(members: []) - ), - parent: nil - ) - - var knownTypeToNominal: [KnownStandardLibraryType: SwiftNominalTypeDeclaration] = [:] - var nominalTypeDeclToKnownType: [SwiftNominalTypeDeclaration: KnownStandardLibraryType] = [:] - - // Handle all of the non-generic types at once. - for knownType in KnownStandardLibraryType.allCases { - guard !knownType.isGeneric else { - continue - } - - Self.recordKnownNonGenericStruct( - knownType, - knownTypeToNominal: &knownTypeToNominal, - nominalTypeDeclToKnownType: &nominalTypeDeclToKnownType, - parsedModule: &parsedModule - ) - } - - self.knownTypeToNominal = knownTypeToNominal - self.nominalTypeDeclToKnownType = nominalTypeDeclToKnownType - } - - subscript(knownType: KnownStandardLibraryType) -> SwiftNominalTypeDeclaration { - knownTypeToNominal[knownType]! - } - - subscript(nominalType: SwiftNominalTypeDeclaration) -> KnownStandardLibraryType? { - nominalTypeDeclToKnownType[nominalType] - } -} diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftSymbolTable.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftSymbolTable.swift index 87c2c80f8..ef299bab3 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftSymbolTable.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftSymbolTable.swift @@ -38,89 +38,58 @@ extension SwiftSymbolTableProtocol { } package class SwiftSymbolTable { - var importedModules: [SwiftModuleSymbolTable] = [] - var parsedModule: SwiftParsedModuleSymbolTable + let importedModules: [String: SwiftModuleSymbolTable] + let parsedModule: SwiftModuleSymbolTable - package init(parsedModuleName: String) { - self.parsedModule = SwiftParsedModuleSymbolTable(moduleName: parsedModuleName) + private var knownTypeToNominal: [SwiftKnownTypeDeclKind: SwiftNominalTypeDeclaration] = [:] + private var prioritySortedImportedModules: [SwiftModuleSymbolTable] { + importedModules.values.sorted(by: { ($0.alternativeModules?.isMainSourceOfSymbols ?? false) && $0.moduleName < $1.moduleName }) } - func addImportedModule(symbolTable: SwiftModuleSymbolTable) { - importedModules.append(symbolTable) + init(parsedModule: SwiftModuleSymbolTable, importedModules: [String: SwiftModuleSymbolTable]) { + self.parsedModule = parsedModule + self.importedModules = importedModules } } extension SwiftSymbolTable { - package func setup(_ sourceFiles: some Collection) { - // First, register top-level and nested nominal types to the symbol table. - for sourceFile in sourceFiles { - self.addNominalTypeDeclarations(sourceFile) - } - - // Next bind the extensions. - - // The work queue is required because, the extending type might be declared - // in another extension that hasn't been processed. E.g.: - // - // extension Outer.Inner { struct Deeper {} } - // extension Outer { struct Inner {} } - // struct Outer {} - // - var unresolvedExtensions: [ExtensionDeclSyntax] = [] - for sourceFile in sourceFiles { - // Find extensions. - for statement in sourceFile.statements { - // We only care about extensions at top-level. - if case .decl(let decl) = statement.item, let extNode = decl.as(ExtensionDeclSyntax.self) { - let resolved = handleExtension(extNode) - if !resolved { - unresolvedExtensions.append(extNode) - } - } - } + package static func setup( + moduleName: String, + _ inputFiles: some Collection, + log: Logger + ) -> SwiftSymbolTable { + + // Prepare imported modules. + // FIXME: Support arbitrary dependencies. + var modules: Set = [] + for inputFile in inputFiles { + let importedModules = importingModules(sourceFile: inputFile.syntax) + modules.formUnion(importedModules) } - - while !unresolvedExtensions.isEmpty { - let numExtensionsBefore = unresolvedExtensions.count - unresolvedExtensions.removeAll(where: handleExtension(_:)) - - // If we didn't resolve anything, we're done. - if numExtensionsBefore == unresolvedExtensions.count { - break + var importedModules: [String: SwiftModuleSymbolTable] = [:] + importedModules[SwiftKnownModule.swift.name] = SwiftKnownModule.swift.symbolTable + for module in modules { + // We don't need duplicates of symbols, first known definition is enough to parse module + // e.g Data from FoundationEssentials and Foundation collide and lead to different results due to random order of keys in Swift's Dictionary + // guard module.isMainSourceOfSymbols || !importedModules.contains(where: { $0.value.isAlternative(for: String)}) else { continue } + + if + importedModules[module.name] == nil, + let knownModule = SwiftKnownModule(rawValue: module.name) + { + importedModules[module.name] = knownModule.symbolTable } - assert(numExtensionsBefore > unresolvedExtensions.count) } - } - - private func addNominalTypeDeclarations(_ sourceFile: SourceFileSyntax) { - // Find top-level nominal type declarations. - for statement in sourceFile.statements { - // We only care about declarations. - guard case .decl(let decl) = statement.item, - let nominalTypeNode = decl.asNominal else { - continue - } - parsedModule.addNominalTypeDeclaration(nominalTypeNode, parent: nil) - } - } + // FIXME: Support granular lookup context (file, type context). - private func handleExtension(_ extensionDecl: ExtensionDeclSyntax) -> Bool { - // Try to resolve the type referenced by this extension declaration. - // If it fails, we'll try again later. - guard let extendedType = try? SwiftType(extensionDecl.extendedType, symbolTable: self) else { - return false - } - guard let extendedNominal = extendedType.asNominalTypeDeclaration else { - // Extending type was not a nominal type. Ignore it. - return true + var builder = SwiftParsedModuleSymbolTableBuilder(moduleName: moduleName, importedModules: importedModules, log: log) + // First, register top-level and nested nominal types to the symbol table. + for sourceFile in inputFiles { + builder.handle(sourceFile: sourceFile.syntax, sourceFilePath: sourceFile.path) } - - // Register nested nominals in extensions to the symbol table. - parsedModule.addExtension(extensionDecl, extending: extendedNominal) - - // We have successfully resolved the extended type. Record it. - return true + let parsedModule = builder.finalize() + return SwiftSymbolTable(parsedModule: parsedModule, importedModules: importedModules) } } @@ -134,12 +103,14 @@ extension SwiftSymbolTable: SwiftSymbolTableProtocol { return parsedResult } - for importedModule in importedModules { + for importedModule in prioritySortedImportedModules { if let result = importedModule.lookupTopLevelNominalType(name) { return result } } + // FIXME: Implement module qualified name lookups. E.g. 'Swift.String' + return nil } @@ -149,7 +120,7 @@ extension SwiftSymbolTable: SwiftSymbolTableProtocol { return parsedResult } - for importedModule in importedModules { + for importedModule in importedModules.values { if let result = importedModule.lookupNestedType(name, parent: parent) { return result } @@ -158,3 +129,21 @@ extension SwiftSymbolTable: SwiftSymbolTableProtocol { return nil } } + +extension SwiftSymbolTable { + /// Map 'SwiftKnownTypeDeclKind' to the declaration. + subscript(knownType: SwiftKnownTypeDeclKind) -> SwiftNominalTypeDeclaration! { + if let known = knownTypeToNominal[knownType] { + return known + } + + let (module, name) = knownType.moduleAndName + guard let moduleTable = importedModules[module] else { + return nil + } + + let found = moduleTable.lookupTopLevelNominalType(name) + knownTypeToNominal[knownType] = found + return found + } +} diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftType+GenericTypes.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftType+GenericTypes.swift new file mode 100644 index 000000000..1a658513d --- /dev/null +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftType+GenericTypes.swift @@ -0,0 +1,97 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +extension SwiftType { + /// Returns a concrete type if this is a generic parameter in the list and it + /// conforms to a protocol with representative concrete type. + func representativeConcreteTypeIn( + knownTypes: SwiftKnownTypes, + genericParameters: [SwiftGenericParameterDeclaration], + genericRequirements: [SwiftGenericRequirement] + ) -> SwiftType? { + return representativeConcreteType( + self, + knownTypes: knownTypes, + genericParameters: genericParameters, + genericRequirements: genericRequirements + ) + } + + /// Returns the protocol type if this is a generic parameter in the list + func typeIn( + genericParameters: [SwiftGenericParameterDeclaration], + genericRequirements: [SwiftGenericRequirement] + ) -> SwiftType? { + switch self { + case .genericParameter(let genericParam): + if genericParameters.contains(genericParam) { + let types: [SwiftType] = genericRequirements.compactMap { + guard case .inherits(let left, let right) = $0, left == self else { + return nil + } + return right + } + + if types.isEmpty { + // TODO: Any?? + return nil + } else if types.count == 1 { + return types.first! + } else { + return .composite(types) + } + } + + return nil + + default: + return nil + } + } +} + +private func representativeConcreteType( + _ type: SwiftType, + knownTypes: SwiftKnownTypes, + genericParameters: [SwiftGenericParameterDeclaration], + genericRequirements: [SwiftGenericRequirement] +) -> SwiftType? { + var maybeProto: SwiftType? = nil + switch type { + case .existential(let proto), .opaque(let proto): + maybeProto = proto + case .genericParameter(let genericParam): + // If the type is a generic parameter declared in this function and + // conforms to a protocol with representative concrete type, use it. + if genericParameters.contains(genericParam) { + for requirement in genericRequirements { + if case .inherits(let left, let right) = requirement, left == type { + guard maybeProto == nil else { + // multiple requirements on the generic parameter. + return nil + } + maybeProto = right + break + } + } + } + default: + return nil + } + + if let knownProtocol = maybeProto?.asNominalTypeDeclaration?.knownTypeKind { + return knownTypes.representativeType(of: knownProtocol) + } + return nil +} diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift index 5b0d89612..aeb88bfba 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift @@ -16,12 +16,33 @@ import SwiftSyntax /// Describes a type in the Swift type system. enum SwiftType: Equatable { + case nominal(SwiftNominalType) + + case genericParameter(SwiftGenericParameterDeclaration) + indirect case function(SwiftFunctionType) + + /// `.Type` indirect case metatype(SwiftType) - case nominal(SwiftNominalType) + + /// `?` indirect case optional(SwiftType) + + /// `(, )` case tuple([SwiftType]) + /// `any ` + indirect case existential(SwiftType) + + /// `some ` + indirect case opaque(SwiftType) + + /// `type1` & `type2` + indirect case composite([SwiftType]) + + /// `[type]` + indirect case array(SwiftType) + static var void: Self { return .tuple([]) } @@ -30,7 +51,7 @@ enum SwiftType: Equatable { switch self { case .nominal(let nominal): nominal case .tuple(let elements): elements.count == 1 ? elements[0].asNominalType : nil - case .function, .metatype, .optional: nil + case .genericParameter, .function, .metatype, .optional, .existential, .opaque, .composite, .array: nil } } @@ -38,8 +59,7 @@ enum SwiftType: Equatable { asNominalType?.nominalTypeDecl } - /// Whether this is the "Void" type, which is actually an empty - /// tuple. + /// Whether this is the "Void" type, which is actually an empty tuple. var isVoid: Bool { switch self { case .tuple([]): @@ -51,6 +71,19 @@ enum SwiftType: Equatable { } } + /// Whether this is a pointer type. I.e 'Unsafe[Mutable][Raw]Pointer' + var isPointer: Bool { + switch self { + case .nominal(let nominal): + if let knownType = nominal.nominalTypeDecl.knownTypeKind { + return knownType.isPointer + } + default: + break + } + return false; + } + /// Reference type /// /// * Mutations don't require 'inout' convention. @@ -61,10 +94,34 @@ enum SwiftType: Equatable { return nominal.nominalTypeDecl.isReferenceType case .metatype, .function: return true - case .optional, .tuple: + case .genericParameter, .optional, .tuple, .existential, .opaque, .composite, .array: return false } } + + var isUnsignedInteger: Bool { + switch self { + case .nominal(let nominal): + switch nominal.nominalTypeDecl.knownTypeKind { + case .uint8, .uint16, .uint32, .uint64: true + default: false + } + default: false + } + } + + var isRawTypeCompatible: Bool { + switch self { + case .nominal(let nominal): + switch nominal.nominalTypeDecl.knownTypeKind { + case .int, .uint, .int8, .uint8, .int16, .uint16, .int32, .uint32, .int64, .uint64, .float, .double, .string: + true + default: + false + } + default: false + } + } } extension SwiftType: CustomStringConvertible { @@ -72,14 +129,15 @@ extension SwiftType: CustomStringConvertible { /// requires parentheses. private var postfixRequiresParentheses: Bool { switch self { - case .function: true - case .metatype, .nominal, .optional, .tuple: false + case .function, .existential, .opaque, .composite: true + case .genericParameter, .metatype, .nominal, .optional, .tuple, .array: false } } var description: String { switch self { case .nominal(let nominal): return nominal.description + case .genericParameter(let genericParam): return genericParam.name case .function(let functionType): return functionType.description case .metatype(let instanceType): var instanceTypeStr = instanceType.description @@ -91,6 +149,14 @@ extension SwiftType: CustomStringConvertible { return "\(wrappedType.description)?" case .tuple(let elements): return "(\(elements.map(\.description).joined(separator: ", ")))" + case .existential(let constraintType): + return "any \(constraintType)" + case .opaque(let constraintType): + return "some \(constraintType)" + case .composite(let types): + return types.map(\.description).joined(separator: " & ") + case .array(let type): + return "[\(type)]" } } } @@ -142,13 +208,23 @@ extension SwiftNominalType: CustomStringConvertible { } } +extension SwiftNominalType { + // TODO: Better way to detect Java wrapped classes. + var isSwiftJavaWrapper: Bool { + nominalTypeDecl.name.hasPrefix("Java") + } + + var isProtocol: Bool { + nominalTypeDecl.kind == .protocol + } +} + extension SwiftType { - init(_ type: TypeSyntax, symbolTable: SwiftSymbolTable) throws { + init(_ type: TypeSyntax, lookupContext: SwiftTypeLookupContext) throws { switch type.as(TypeSyntaxEnum.self) { - case .arrayType, .classRestrictionType, .compositionType, + case .classRestrictionType, .dictionaryType, .missingType, .namedOpaqueReturnType, - .packElementType, .packExpansionType, .someOrAnyType, - .suppressedType: + .packElementType, .packExpansionType, .suppressedType, .inlineArrayType: throw TypeTranslationError.unimplementedType(type) case .attributedType(let attributedType): @@ -157,7 +233,7 @@ extension SwiftType { // FIXME: This string matching is a horrible hack. switch attributedType.attributes.trimmedDescription { case "@convention(c)", "@convention(swift)": - let innerType = try SwiftType(attributedType.baseType, symbolTable: symbolTable) + let innerType = try SwiftType(attributedType.baseType, lookupContext: lookupContext) switch innerType { case .function(var functionType): let isConventionC = attributedType.attributes.trimmedDescription == "@convention(c)" @@ -173,14 +249,19 @@ extension SwiftType { case .functionType(let functionType): self = .function( - try SwiftFunctionType(functionType, convention: .swift, symbolTable: symbolTable) + try SwiftFunctionType(functionType, convention: .swift, lookupContext: lookupContext) ) case .identifierType(let identifierType): // Translate the generic arguments. let genericArgs = try identifierType.genericArgumentClause.map { genericArgumentClause in try genericArgumentClause.arguments.map { argument in - try SwiftType(argument.argument, symbolTable: symbolTable) + switch argument.argument { + case .type(let argumentTy): + try SwiftType(argumentTy, lookupContext: lookupContext) + default: + throw TypeTranslationError.unimplementedType(type) + } } } @@ -188,13 +269,13 @@ extension SwiftType { self = try SwiftType( originalType: type, parent: nil, - name: identifierType.name.text, + name: identifierType.name, genericArguments: genericArgs, - symbolTable: symbolTable + lookupContext: lookupContext ) case .implicitlyUnwrappedOptionalType(let optionalType): - self = .optional(try SwiftType(optionalType.wrappedType, symbolTable: symbolTable)) + self = .optional(try SwiftType(optionalType.wrappedType, lookupContext: lookupContext)) case .memberType(let memberType): // If the parent type isn't a known module, translate it. @@ -204,59 +285,94 @@ extension SwiftType { if memberType.baseType.trimmedDescription == "Swift" { parentType = nil } else { - parentType = try SwiftType(memberType.baseType, symbolTable: symbolTable) + parentType = try SwiftType(memberType.baseType, lookupContext: lookupContext) } // Translate the generic arguments. let genericArgs = try memberType.genericArgumentClause.map { genericArgumentClause in try genericArgumentClause.arguments.map { argument in - try SwiftType(argument.argument, symbolTable: symbolTable) + switch argument.argument { + case .type(let argumentTy): + try SwiftType(argumentTy, lookupContext: lookupContext) + default: + throw TypeTranslationError.unimplementedType(type) + } } } self = try SwiftType( originalType: type, parent: parentType, - name: memberType.name.text, + name: memberType.name, genericArguments: genericArgs, - symbolTable: symbolTable + lookupContext: lookupContext ) case .metatypeType(let metatypeType): - self = .metatype(try SwiftType(metatypeType.baseType, symbolTable: symbolTable)) + self = .metatype(try SwiftType(metatypeType.baseType, lookupContext: lookupContext)) case .optionalType(let optionalType): - self = .optional(try SwiftType(optionalType.wrappedType, symbolTable: symbolTable)) + self = .optional(try SwiftType(optionalType.wrappedType, lookupContext: lookupContext)) case .tupleType(let tupleType): self = try .tuple(tupleType.elements.map { element in - try SwiftType(element.type, symbolTable: symbolTable) + try SwiftType(element.type, lookupContext: lookupContext) }) + + case .someOrAnyType(let someOrAntType): + if someOrAntType.someOrAnySpecifier.tokenKind == .keyword(.some) { + self = .opaque(try SwiftType(someOrAntType.constraint, lookupContext: lookupContext)) + } else { + self = .opaque(try SwiftType(someOrAntType.constraint, lookupContext: lookupContext)) + } + + case .compositionType(let compositeType): + let types = try compositeType.elements.map { + try SwiftType($0.type, lookupContext: lookupContext) + } + + self = .composite(types) + + case .arrayType(let arrayType): + let elementType = try SwiftType(arrayType.element, lookupContext: lookupContext) + self = .array(elementType) } } init( originalType: TypeSyntax, parent: SwiftType?, - name: String, + name: TokenSyntax, genericArguments: [SwiftType]?, - symbolTable: SwiftSymbolTable + lookupContext: SwiftTypeLookupContext ) throws { // Look up the imported types by name to resolve it to a nominal type. - guard let nominalTypeDecl = symbolTable.lookupType( - name, - parent: parent?.asNominalTypeDeclaration - ) else { - throw TypeTranslationError.unknown(originalType) + let typeDecl: SwiftTypeDeclaration? + if let parent { + guard let parentDecl = parent.asNominalTypeDeclaration else { + throw TypeTranslationError.unknown(originalType) + } + typeDecl = lookupContext.symbolTable.lookupNestedType(name.text, parent: parentDecl) + } else { + typeDecl = try lookupContext.unqualifiedLookup(name: Identifier(name)!, from: name) + } + guard let typeDecl else { + throw TypeTranslationError.unknown(originalType) } - self = .nominal( - SwiftNominalType( - parent: parent?.asNominalType, - nominalTypeDecl: nominalTypeDecl, - genericArguments: genericArguments + if let nominalDecl = typeDecl as? SwiftNominalTypeDeclaration { + self = .nominal( + SwiftNominalType( + parent: parent?.asNominalType, + nominalTypeDecl: nominalDecl, + genericArguments: genericArguments + ) ) - ) + } else if let genericParamDecl = typeDecl as? SwiftGenericParameterDeclaration { + self = .genericParameter(genericParamDecl) + } else { + fatalError("unknown SwiftTypeDeclaration: \(type(of: typeDecl))") + } } init?( @@ -293,11 +409,11 @@ extension SwiftType { enum TypeTranslationError: Error { /// We haven't yet implemented support for this type. - case unimplementedType(TypeSyntax) + case unimplementedType(TypeSyntax, file: StaticString = #file, line: Int = #line) /// Missing generic arguments. - case missingGenericArguments(TypeSyntax) + case missingGenericArguments(TypeSyntax, file: StaticString = #file, line: Int = #line) /// Unknown nominal type. - case unknown(TypeSyntax) + case unknown(TypeSyntax, file: StaticString = #file, line: Int = #line) } diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftTypeLookupContext.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftTypeLookupContext.swift new file mode 100644 index 000000000..4489b4f65 --- /dev/null +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftTypeLookupContext.swift @@ -0,0 +1,167 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftSyntax +@_spi(Experimental) import SwiftLexicalLookup + +/// Unqualified type lookup manager. +/// All unqualified lookup should be done via this instance. This caches the +/// association of `Syntax.ID` to `SwiftTypeDeclaration`, and guarantees that +/// there's only one `SwiftTypeDeclaration` per declaration `Syntax`. +class SwiftTypeLookupContext { + var symbolTable: SwiftSymbolTable + + private var typeDecls: [Syntax.ID: SwiftTypeDeclaration] = [:] + + init(symbolTable: SwiftSymbolTable) { + self.symbolTable = symbolTable + } + + /// Perform unqualified type lookup. + /// + /// - Parameters: + /// - name: name to lookup + /// - node: `Syntax` node the lookup happened + func unqualifiedLookup(name: Identifier, from node: some SyntaxProtocol) throws -> SwiftTypeDeclaration? { + + for result in node.lookup(name) { + switch result { + case .fromScope(_, let names): + if !names.isEmpty { + return typeDeclaration(for: names) + } + + case .lookForMembers(let scopeNode): + if let nominalDecl = try typeDeclaration(for: scopeNode, sourceFilePath: "FIXME.swift") { // FIXME: no path here // implement some node -> file + if let found = symbolTable.lookupNestedType(name.name, parent: nominalDecl as! SwiftNominalTypeDeclaration) { + return found + } + } + + case .lookForGenericParameters(let extensionNode): + // TODO: Implement + _ = extensionNode + break + + case .lookForImplicitClosureParameters: + // Dollar identifier can't be a type, ignore. + break + } + } + + // Fallback to global symbol table lookup. + return symbolTable.lookupTopLevelNominalType(name.name) + } + + /// Find the first type declaration in the `LookupName` results. + private func typeDeclaration(for names: [LookupName]) -> SwiftTypeDeclaration? { + for name in names { + switch name { + case .identifier(let identifiableSyntax, _): + return try? typeDeclaration(for: identifiableSyntax, sourceFilePath: "FIXME_NO_PATH.swift") // FIXME: how to get path here? + case .declaration(let namedDeclSyntax): + return try? typeDeclaration(for: namedDeclSyntax, sourceFilePath: "FIXME_NO_PATH.swift") // FIXME: how to get path here? + case .implicit(let implicitDecl): + // TODO: Implement + _ = implicitDecl + break + case .equivalentNames(let equivalentNames): + // TODO: Implement + _ = equivalentNames + } + } + return nil + } + + /// Returns the type declaration object associated with the `Syntax` node. + /// If there's no declaration created, create an instance on demand, and cache it. + func typeDeclaration(for node: some SyntaxProtocol, sourceFilePath: String) throws -> SwiftTypeDeclaration? { + if let found = typeDecls[node.id] { + return found + } + + let typeDecl: SwiftTypeDeclaration + switch Syntax(node).as(SyntaxEnum.self) { + case .genericParameter(let node): + typeDecl = SwiftGenericParameterDeclaration(sourceFilePath: sourceFilePath, moduleName: symbolTable.moduleName, node: node) + case .classDecl(let node): + typeDecl = try nominalTypeDeclaration(for: node, sourceFilePath: sourceFilePath) + case .actorDecl(let node): + typeDecl = try nominalTypeDeclaration(for: node, sourceFilePath: sourceFilePath) + case .structDecl(let node): + typeDecl = try nominalTypeDeclaration(for: node, sourceFilePath: sourceFilePath) + case .enumDecl(let node): + typeDecl = try nominalTypeDeclaration(for: node, sourceFilePath: sourceFilePath) + case .protocolDecl(let node): + typeDecl = try nominalTypeDeclaration(for: node, sourceFilePath: sourceFilePath) + case .extensionDecl(let node): + // For extensions, we have to perform a unqualified lookup, + // as the extentedType is just the identifier of the type. + + guard case .identifierType(let id) = Syntax(node.extendedType).as(SyntaxEnum.self), + let lookupResult = try unqualifiedLookup(name: Identifier(id.name)!, from: node) + else { + throw TypeLookupError.notType(Syntax(node)) + } + + typeDecl = lookupResult + case .typeAliasDecl: + fatalError("typealias not implemented") + case .associatedTypeDecl: + fatalError("associatedtype not implemented") + default: + throw TypeLookupError.notType(Syntax(node)) + } + + typeDecls[node.id] = typeDecl + return typeDecl + } + + /// Create a nominal type declaration instance for the specified syntax node. + private func nominalTypeDeclaration(for node: NominalTypeDeclSyntaxNode, sourceFilePath: String) throws -> SwiftNominalTypeDeclaration { + + if let symbolTableDeclaration = self.symbolTable.lookupType( + node.name.text, + parent: try parentTypeDecl(for: node) + ) { + return symbolTableDeclaration + } + + return SwiftNominalTypeDeclaration( + sourceFilePath: sourceFilePath, + moduleName: self.symbolTable.moduleName, + parent: try parentTypeDecl(for: node), + node: node + ) + } + + /// Find a parent nominal type declaration of the specified syntax node. + private func parentTypeDecl(for node: some DeclSyntaxProtocol) throws -> SwiftNominalTypeDeclaration? { + var node: DeclSyntax = DeclSyntax(node) + while let parentDecl = node.ancestorDecl { + switch parentDecl.as(DeclSyntaxEnum.self) { + case .structDecl, .classDecl, .actorDecl, .enumDecl, .protocolDecl: + return (try typeDeclaration(for: parentDecl, sourceFilePath: "FIXME_NO_SOURCE_FILE.swift") as! SwiftNominalTypeDeclaration) // FIXME: need to get the source file of the parent + default: + node = parentDecl + continue + } + } + return nil + } +} + +enum TypeLookupError: Error { + case notType(Syntax) +} diff --git a/Sources/JExtractSwiftLib/ThunkNameRegistry.swift b/Sources/JExtractSwiftLib/ThunkNameRegistry.swift index 3369ec629..da2e95c1b 100644 --- a/Sources/JExtractSwiftLib/ThunkNameRegistry.swift +++ b/Sources/JExtractSwiftLib/ThunkNameRegistry.swift @@ -32,9 +32,9 @@ package struct ThunkNameRegistry { let suffix: String switch decl.apiKind { - case .getter: + case .getter, .subscriptGetter: suffix = "$get" - case .setter: + case .setter, .subscriptSetter: suffix = "$set" default: suffix = decl.functionSignature.parameters diff --git a/Sources/JavaKit/BridgedValues/JavaValue+Integers.swift b/Sources/JavaKit/BridgedValues/JavaValue+Integers.swift deleted file mode 100644 index 01a7bdbac..000000000 --- a/Sources/JavaKit/BridgedValues/JavaValue+Integers.swift +++ /dev/null @@ -1,287 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift.org project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift.org project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import JavaTypes - -extension Int8: JavaValue { - public typealias JNIType = jbyte - - public static var jvalueKeyPath: WritableKeyPath { \.b } - - public static var javaType: JavaType { .byte } - - public static func jniMethodCall( - in environment: JNIEnvironment - ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { - environment.interface.CallByteMethodA - } - - public static func jniFieldGet(in environment: JNIEnvironment) -> JNIFieldGet { - environment.interface.GetByteField - } - - public static func jniFieldSet(in environment: JNIEnvironment) -> JNIFieldSet { - environment.interface.SetByteField - } - - public static func jniStaticMethodCall( - in environment: JNIEnvironment - ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { - environment.interface.CallStaticByteMethodA - } - - public static func jniStaticFieldGet(in environment: JNIEnvironment) -> JNIStaticFieldGet { - environment.interface.GetStaticByteField - } - - public static func jniStaticFieldSet(in environment: JNIEnvironment) -> JNIStaticFieldSet { - environment.interface.SetStaticByteField - } - - public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray { - environment.interface.NewByteArray - } - - public static func jniGetArrayRegion(in environment: JNIEnvironment) -> JNIGetArrayRegion { - environment.interface.GetByteArrayRegion - } - - public static func jniSetArrayRegion(in environment: JNIEnvironment) -> JNISetArrayRegion { - environment.interface.SetByteArrayRegion - } - - public static var jniPlaceholderValue: jbyte { - 0 - } -} - -extension UInt16: JavaValue { - public typealias JNIType = jchar - - public static var jvalueKeyPath: WritableKeyPath { \.c } - - public static var javaType: JavaType { .char } - - public static func jniMethodCall( - in environment: JNIEnvironment - ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { - environment.interface.CallCharMethodA - } - - public static func jniFieldGet(in environment: JNIEnvironment) -> JNIFieldGet { - environment.interface.GetCharField - } - - public static func jniFieldSet(in environment: JNIEnvironment) -> JNIFieldSet { - environment.interface.SetCharField - } - - public static func jniStaticMethodCall( - in environment: JNIEnvironment - ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { - environment.interface.CallStaticCharMethodA - } - - public static func jniStaticFieldGet(in environment: JNIEnvironment) -> JNIStaticFieldGet { - environment.interface.GetStaticCharField - } - - public static func jniStaticFieldSet(in environment: JNIEnvironment) -> JNIStaticFieldSet { - environment.interface.SetStaticCharField - } - - public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray { - environment.interface.NewCharArray - } - - public static func jniGetArrayRegion(in environment: JNIEnvironment) -> JNIGetArrayRegion { - environment.interface.GetCharArrayRegion - } - - public static func jniSetArrayRegion(in environment: JNIEnvironment) -> JNISetArrayRegion { - environment.interface.SetCharArrayRegion - } - - public static var jniPlaceholderValue: jchar { - 0 - } -} - -extension Int16: JavaValue { - public typealias JNIType = jshort - - public static var jvalueKeyPath: WritableKeyPath { \.s } - - public static var javaType: JavaType { .short } - - public static func jniMethodCall( - in environment: JNIEnvironment - ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { - environment.interface.CallShortMethodA - } - - public static func jniFieldGet(in environment: JNIEnvironment) -> JNIFieldGet { - environment.interface.GetShortField - } - - public static func jniFieldSet(in environment: JNIEnvironment) -> JNIFieldSet { - environment.interface.SetShortField - } - - public static func jniStaticMethodCall( - in environment: JNIEnvironment - ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { - environment.interface.CallStaticShortMethodA - } - - public static func jniStaticFieldGet(in environment: JNIEnvironment) -> JNIStaticFieldGet { - environment.interface.GetStaticShortField - } - - public static func jniStaticFieldSet(in environment: JNIEnvironment) -> JNIStaticFieldSet { - environment.interface.SetStaticShortField - } - - public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray { - environment.interface.NewShortArray - } - - public static func jniGetArrayRegion(in environment: JNIEnvironment) -> JNIGetArrayRegion { - environment.interface.GetShortArrayRegion - } - - public static func jniSetArrayRegion(in environment: JNIEnvironment) -> JNISetArrayRegion { - environment.interface.SetShortArrayRegion - } - - public static var jniPlaceholderValue: jshort { - 0 - } -} - -extension Int32: JavaValue { - public typealias JNIType = jint - - public static var jvalueKeyPath: WritableKeyPath { \.i } - - public func getJNIValue(in environment: JNIEnvironment) -> JNIType { JNIType(self) } - - public init(fromJNI value: JNIType, in environment: JNIEnvironment) { - self = Int32(value) - } - - public static var javaType: JavaType { .int } - - public static func jniMethodCall( - in environment: JNIEnvironment - ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { - environment.interface.CallIntMethodA - } - - public static func jniFieldGet(in environment: JNIEnvironment) -> JNIFieldGet { - environment.interface.GetIntField - } - - public static func jniFieldSet(in environment: JNIEnvironment) -> JNIFieldSet { - environment.interface.SetIntField - } - - public static func jniStaticMethodCall( - in environment: JNIEnvironment - ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { - environment.interface.CallStaticIntMethodA - } - - public static func jniStaticFieldGet(in environment: JNIEnvironment) -> JNIStaticFieldGet { - environment.interface.GetStaticIntField - } - - public static func jniStaticFieldSet(in environment: JNIEnvironment) -> JNIStaticFieldSet { - environment.interface.SetStaticIntField - } - - public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray { - environment.interface.NewIntArray - } - - public static func jniGetArrayRegion(in environment: JNIEnvironment) -> JNIGetArrayRegion { - environment.interface.GetIntArrayRegion - } - - public static func jniSetArrayRegion(in environment: JNIEnvironment) -> JNISetArrayRegion { - environment.interface.SetIntArrayRegion - } - - public static var jniPlaceholderValue: jint { - 0 - } -} - -extension Int64: JavaValue { - public typealias JNIType = jlong - - public static var jvalueKeyPath: WritableKeyPath { \.j } - - public func getJNIValue(in environment: JNIEnvironment) -> JNIType { JNIType(self) } - - public init(fromJNI value: JNIType, in environment: JNIEnvironment) { - self = Int64(value) - } - - public static var javaType: JavaType { .long } - - public static func jniMethodCall( - in environment: JNIEnvironment - ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { - environment.interface.CallLongMethodA - } - - public static func jniFieldGet(in environment: JNIEnvironment) -> JNIFieldGet { - environment.interface.GetLongField - } - - public static func jniFieldSet(in environment: JNIEnvironment) -> JNIFieldSet { - environment.interface.SetLongField - } - - public static func jniStaticMethodCall( - in environment: JNIEnvironment - ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { - environment.interface.CallStaticLongMethodA - } - - public static func jniStaticFieldGet(in environment: JNIEnvironment) -> JNIStaticFieldGet { - environment.interface.GetStaticLongField - } - - public static func jniStaticFieldSet(in environment: JNIEnvironment) -> JNIStaticFieldSet { - environment.interface.SetStaticLongField - } - - public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray { - environment.interface.NewLongArray - } - - public static func jniGetArrayRegion(in environment: JNIEnvironment) -> JNIGetArrayRegion { - environment.interface.GetLongArrayRegion - } - - public static func jniSetArrayRegion(in environment: JNIEnvironment) -> JNISetArrayRegion { - environment.interface.SetLongArrayRegion - } - - public static var jniPlaceholderValue: jlong { - 0 - } -} diff --git a/Sources/JavaKit/Helpers/_JNIMethodIDCache.swift b/Sources/JavaKit/Helpers/_JNIMethodIDCache.swift new file mode 100644 index 000000000..a67d225f5 --- /dev/null +++ b/Sources/JavaKit/Helpers/_JNIMethodIDCache.swift @@ -0,0 +1,59 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +/// A cache used to hold references for JNI method and classes. +/// +/// This type is used internally in by the outputted JExtract wrappers +/// to improve performance of any JNI lookups. +public final class _JNIMethodIDCache: Sendable { + public struct Method: Hashable { + public let name: String + public let signature: String + + public init(name: String, signature: String) { + self.name = name + self.signature = signature + } + } + + nonisolated(unsafe) let _class: jclass? + nonisolated(unsafe) let methods: [Method: jmethodID] + + public var javaClass: jclass { + self._class! + } + + public init(environment: UnsafeMutablePointer!, className: String, methods: [Method]) { + guard let clazz = environment.interface.FindClass(environment, className) else { + fatalError("Class \(className) could not be found!") + } + self._class = environment.interface.NewGlobalRef(environment, clazz)! + self.methods = methods.reduce(into: [:]) { (result, method) in + if let methodID = environment.interface.GetMethodID(environment, clazz, method.name, method.signature) { + result[method] = methodID + } else { + fatalError("Method \(method.signature) with signature \(method.signature) not found in class \(className)") + } + } + } + + + public subscript(_ method: Method) -> jmethodID? { + methods[method] + } + + public func cleanup(environment: UnsafeMutablePointer!) { + environment.interface.DeleteGlobalRef(environment, self._class) + } +} diff --git a/Sources/JavaKitDependencyResolver/DependencyResolver.swift b/Sources/JavaKitDependencyResolver/DependencyResolver.swift index 697e3d2a4..2d339b151 100644 --- a/Sources/JavaKitDependencyResolver/DependencyResolver.swift +++ b/Sources/JavaKitDependencyResolver/DependencyResolver.swift @@ -12,10 +12,10 @@ // //===----------------------------------------------------------------------===// -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI -@JavaInterface("org.swift.javakit.dependencies.DependencyResolver") +@JavaInterface("org.swift.jni.dependencies.DependencyResolver") public struct DependencyResolver { } diff --git a/Sources/JavaKitDependencyResolver/swift-java.config b/Sources/JavaKitDependencyResolver/swift-java.config index 6ea5aa879..3803b5f0a 100644 --- a/Sources/JavaKitDependencyResolver/swift-java.config +++ b/Sources/JavaKitDependencyResolver/swift-java.config @@ -1,5 +1,5 @@ { "dependencies": [ - ":JavaKit", + ":SwiftJNI", ] } diff --git a/Sources/JavaKitReflection/generated/ParameterizedType.swift b/Sources/JavaKitReflection/generated/ParameterizedType.swift deleted file mode 100644 index 3e4137148..000000000 --- a/Sources/JavaKitReflection/generated/ParameterizedType.swift +++ /dev/null @@ -1,18 +0,0 @@ -// Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime - -@JavaInterface("java.lang.reflect.ParameterizedType", extends: Type.self) -public struct ParameterizedType { - @JavaMethod - public func getOwnerType() -> Type! - - @JavaMethod - public func getRawType() -> Type! - - @JavaMethod - public func getActualTypeArguments() -> [Type?] - - @JavaMethod - public func getTypeName() -> String -} diff --git a/Sources/JavaKitIO/generated/BufferedInputStream.swift b/Sources/JavaStdlib/JavaIO/generated/BufferedInputStream.swift similarity index 96% rename from Sources/JavaKitIO/generated/BufferedInputStream.swift rename to Sources/JavaStdlib/JavaIO/generated/BufferedInputStream.swift index 8ea95eeb2..2a211eb9a 100644 --- a/Sources/JavaKitIO/generated/BufferedInputStream.swift +++ b/Sources/JavaStdlib/JavaIO/generated/BufferedInputStream.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaClass("java.io.BufferedInputStream") open class BufferedInputStream: InputStream { diff --git a/Sources/JavaKitIO/generated/Charset.swift b/Sources/JavaStdlib/JavaIO/generated/Charset.swift similarity index 96% rename from Sources/JavaKitIO/generated/Charset.swift rename to Sources/JavaStdlib/JavaIO/generated/Charset.swift index fda054eff..03e85a8c8 100644 --- a/Sources/JavaKitIO/generated/Charset.swift +++ b/Sources/JavaStdlib/JavaIO/generated/Charset.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaClass("java.nio.charset.Charset") open class Charset: JavaObject { diff --git a/Sources/JavaKitIO/generated/Closeable.swift b/Sources/JavaStdlib/JavaIO/generated/Closeable.swift similarity index 81% rename from Sources/JavaKitIO/generated/Closeable.swift rename to Sources/JavaStdlib/JavaIO/generated/Closeable.swift index 6da8e2a91..1df526419 100644 --- a/Sources/JavaKitIO/generated/Closeable.swift +++ b/Sources/JavaStdlib/JavaIO/generated/Closeable.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.io.Closeable") public struct Closeable { diff --git a/Sources/JavaKitIO/generated/File.swift b/Sources/JavaStdlib/JavaIO/generated/File.swift similarity index 98% rename from Sources/JavaKitIO/generated/File.swift rename to Sources/JavaStdlib/JavaIO/generated/File.swift index 5cbdb70ea..68b04efb4 100644 --- a/Sources/JavaKitIO/generated/File.swift +++ b/Sources/JavaStdlib/JavaIO/generated/File.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaClass("java.io.File") open class File: JavaObject { diff --git a/Sources/JavaKitIO/generated/FileDescriptor.swift b/Sources/JavaStdlib/JavaIO/generated/FileDescriptor.swift similarity index 93% rename from Sources/JavaKitIO/generated/FileDescriptor.swift rename to Sources/JavaStdlib/JavaIO/generated/FileDescriptor.swift index 81490e460..4c95b2b3a 100644 --- a/Sources/JavaKitIO/generated/FileDescriptor.swift +++ b/Sources/JavaStdlib/JavaIO/generated/FileDescriptor.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaClass("java.io.FileDescriptor") open class FileDescriptor: JavaObject { diff --git a/Sources/JavaKitIO/generated/FileReader.swift b/Sources/JavaStdlib/JavaIO/generated/FileReader.swift similarity index 95% rename from Sources/JavaKitIO/generated/FileReader.swift rename to Sources/JavaStdlib/JavaIO/generated/FileReader.swift index 5254bdd03..b793a3f6f 100644 --- a/Sources/JavaKitIO/generated/FileReader.swift +++ b/Sources/JavaStdlib/JavaIO/generated/FileReader.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaClass("java.io.FileReader") open class FileReader: InputStreamReader { diff --git a/Sources/JavaKitIO/generated/Flushable.swift b/Sources/JavaStdlib/JavaIO/generated/Flushable.swift similarity index 81% rename from Sources/JavaKitIO/generated/Flushable.swift rename to Sources/JavaStdlib/JavaIO/generated/Flushable.swift index daf621f63..95cfc4488 100644 --- a/Sources/JavaKitIO/generated/Flushable.swift +++ b/Sources/JavaStdlib/JavaIO/generated/Flushable.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.io.Flushable") public struct Flushable { diff --git a/Sources/JavaKitIO/generated/InputStream.swift b/Sources/JavaStdlib/JavaIO/generated/InputStream.swift similarity index 96% rename from Sources/JavaKitIO/generated/InputStream.swift rename to Sources/JavaStdlib/JavaIO/generated/InputStream.swift index 971a610ba..b42a8fd85 100644 --- a/Sources/JavaKitIO/generated/InputStream.swift +++ b/Sources/JavaStdlib/JavaIO/generated/InputStream.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaClass("java.io.InputStream", implements: Closeable.self) open class InputStream: JavaObject { diff --git a/Sources/JavaKitIO/generated/InputStreamReader.swift b/Sources/JavaStdlib/JavaIO/generated/InputStreamReader.swift similarity index 95% rename from Sources/JavaKitIO/generated/InputStreamReader.swift rename to Sources/JavaStdlib/JavaIO/generated/InputStreamReader.swift index 2766aebab..7e55d6337 100644 --- a/Sources/JavaKitIO/generated/InputStreamReader.swift +++ b/Sources/JavaStdlib/JavaIO/generated/InputStreamReader.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaClass("java.io.InputStreamReader") open class InputStreamReader: Reader { diff --git a/Sources/JavaKitIO/generated/OutputStream.swift b/Sources/JavaStdlib/JavaIO/generated/OutputStream.swift similarity index 94% rename from Sources/JavaKitIO/generated/OutputStream.swift rename to Sources/JavaStdlib/JavaIO/generated/OutputStream.swift index e169bfd0c..d499508cf 100644 --- a/Sources/JavaKitIO/generated/OutputStream.swift +++ b/Sources/JavaStdlib/JavaIO/generated/OutputStream.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaClass("java.io.OutputStream", implements: Closeable.self, Flushable.self) open class OutputStream: JavaObject { diff --git a/Sources/JavaKitIO/generated/Path.swift b/Sources/JavaStdlib/JavaIO/generated/Path.swift similarity index 97% rename from Sources/JavaKitIO/generated/Path.swift rename to Sources/JavaStdlib/JavaIO/generated/Path.swift index e93d03814..235d9cef1 100644 --- a/Sources/JavaKitIO/generated/Path.swift +++ b/Sources/JavaStdlib/JavaIO/generated/Path.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.nio.file.Path") public struct Path { diff --git a/Sources/JavaKitIO/generated/Readable.swift b/Sources/JavaStdlib/JavaIO/generated/Readable.swift similarity index 75% rename from Sources/JavaKitIO/generated/Readable.swift rename to Sources/JavaStdlib/JavaIO/generated/Readable.swift index 168259897..8961e18af 100644 --- a/Sources/JavaKitIO/generated/Readable.swift +++ b/Sources/JavaStdlib/JavaIO/generated/Readable.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.lang.Readable") public struct Readable { diff --git a/Sources/JavaKitIO/generated/Reader.swift b/Sources/JavaStdlib/JavaIO/generated/Reader.swift similarity index 95% rename from Sources/JavaKitIO/generated/Reader.swift rename to Sources/JavaStdlib/JavaIO/generated/Reader.swift index 2f6cdfe2c..5d8f77bf5 100644 --- a/Sources/JavaKitIO/generated/Reader.swift +++ b/Sources/JavaStdlib/JavaIO/generated/Reader.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaClass("java.io.Reader", implements: Readable.self, Closeable.self) open class Reader: JavaObject { diff --git a/Sources/JavaKitIO/generated/StringReader.swift b/Sources/JavaStdlib/JavaIO/generated/StringReader.swift similarity index 95% rename from Sources/JavaKitIO/generated/StringReader.swift rename to Sources/JavaStdlib/JavaIO/generated/StringReader.swift index e2af1166c..ae4464ed7 100644 --- a/Sources/JavaKitIO/generated/StringReader.swift +++ b/Sources/JavaStdlib/JavaIO/generated/StringReader.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaClass("java.io.StringReader") open class StringReader: Reader { diff --git a/Sources/JavaKitIO/generated/WatchService.swift b/Sources/JavaStdlib/JavaIO/generated/WatchService.swift similarity index 83% rename from Sources/JavaKitIO/generated/WatchService.swift rename to Sources/JavaStdlib/JavaIO/generated/WatchService.swift index 20bca06fc..e2c570a30 100644 --- a/Sources/JavaKitIO/generated/WatchService.swift +++ b/Sources/JavaStdlib/JavaIO/generated/WatchService.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.nio.file.WatchService", extends: Closeable.self) public struct WatchService { diff --git a/Sources/JavaKitIO/generated/Writer.swift b/Sources/JavaStdlib/JavaIO/generated/Writer.swift similarity index 96% rename from Sources/JavaKitIO/generated/Writer.swift rename to Sources/JavaStdlib/JavaIO/generated/Writer.swift index 5e3fdff2a..fe20d27d5 100644 --- a/Sources/JavaKitIO/generated/Writer.swift +++ b/Sources/JavaStdlib/JavaIO/generated/Writer.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaClass("java.io.Writer", implements: Appendable.self, Closeable.self, Flushable.self) open class Writer: JavaObject { diff --git a/Sources/JavaKitIO/swift-java.config b/Sources/JavaStdlib/JavaIO/swift-java.config similarity index 100% rename from Sources/JavaKitIO/swift-java.config rename to Sources/JavaStdlib/JavaIO/swift-java.config index 7b40b2da1..571e02162 100644 --- a/Sources/JavaKitIO/swift-java.config +++ b/Sources/JavaStdlib/JavaIO/swift-java.config @@ -10,10 +10,10 @@ "java.lang.Readable" : "Readable", "java.io.Writer" : "Writer", "java.io.File" : "File", + "java.io.Closeable" : "Closeable", "java.nio.file.Path" : "Path", "java.io.FileDescriptor" : "FileDescriptor", "java.nio.charset.Charset" : "Charset", - "java.io.Closeable" : "Closeable", "java.io.Flushable" : "Flushable", "java.io.Flushable" : "ByteBuffer", "java.nio.file.WatchService" : "WatchService", diff --git a/Sources/JavaKitReflection/Constructor+Utilities.swift b/Sources/JavaStdlib/JavaLangReflect/Constructor+Utilities.swift similarity index 84% rename from Sources/JavaKitReflection/Constructor+Utilities.swift rename to Sources/JavaStdlib/JavaLangReflect/Constructor+Utilities.swift index 8f57ffa51..4042ec766 100644 --- a/Sources/JavaKitReflection/Constructor+Utilities.swift +++ b/Sources/JavaStdlib/JavaLangReflect/Constructor+Utilities.swift @@ -13,11 +13,6 @@ //===----------------------------------------------------------------------===// extension Constructor { - /// Whether this is a 'public' constructor. - public var isPublic: Bool { - return (getModifiers() & 1) != 0 - } - /// Whether this is a 'native' constructor. public var isNative: Bool { return (getModifiers() & 256) != 0 diff --git a/Sources/JavaKitReflection/Executable+Utilities.swift b/Sources/JavaStdlib/JavaLangReflect/Executable+Utilities.swift similarity index 98% rename from Sources/JavaKitReflection/Executable+Utilities.swift rename to Sources/JavaStdlib/JavaLangReflect/Executable+Utilities.swift index 2b0a8a2db..bf70f1146 100644 --- a/Sources/JavaKitReflection/Executable+Utilities.swift +++ b/Sources/JavaStdlib/JavaLangReflect/Executable+Utilities.swift @@ -12,7 +12,7 @@ // //===----------------------------------------------------------------------===// -import JavaKit +import SwiftJava extension Executable { /// Whether this executable throws any checked exception. diff --git a/Sources/JavaKitReflection/Field+Utilities.swift b/Sources/JavaStdlib/JavaLangReflect/Field+Utilities.swift similarity index 100% rename from Sources/JavaKitReflection/Field+Utilities.swift rename to Sources/JavaStdlib/JavaLangReflect/Field+Utilities.swift diff --git a/Sources/JavaStdlib/JavaLangReflect/HasJavaModifiers.swift b/Sources/JavaStdlib/JavaLangReflect/HasJavaModifiers.swift new file mode 100644 index 000000000..fc10edfea --- /dev/null +++ b/Sources/JavaStdlib/JavaLangReflect/HasJavaModifiers.swift @@ -0,0 +1,47 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftJava + +public protocol HasJavaModifiers { + func getModifiers() -> Int32 +} + +extension HasJavaModifiers { + /// Whether the modifiers contain 'public'. + public var isPublic: Bool { + return (getModifiers() & 0x00000001) != 0 + } + + /// Whether the modifiers contain 'private'. + public var isPrivate: Bool { + return (getModifiers() & 0x00000002) != 0 + } + + /// Whether the modifiers contain 'protected'. + public var isProtected: Bool { + return (getModifiers() & 0x00000004) != 0 + } + + /// Whether the modifiers is equivelant to 'package'.. + /// + /// The "default" access level in Java is 'package', it is signified by lack of a different access modifier. + public var isPackage: Bool { + return !isPublic && !isPrivate && !isProtected + } +} + +extension Constructor: HasJavaModifiers {} +extension JavaClass: HasJavaModifiers {} +extension Method: HasJavaModifiers {} diff --git a/Sources/JavaKitReflection/JavaClass+Reflection.swift b/Sources/JavaStdlib/JavaLangReflect/JavaClass+Reflection.swift similarity index 98% rename from Sources/JavaKitReflection/JavaClass+Reflection.swift rename to Sources/JavaStdlib/JavaLangReflect/JavaClass+Reflection.swift index 1f2120e00..29e24be5b 100644 --- a/Sources/JavaKitReflection/JavaClass+Reflection.swift +++ b/Sources/JavaStdlib/JavaLangReflect/JavaClass+Reflection.swift @@ -12,7 +12,7 @@ // //===----------------------------------------------------------------------===// -import JavaKit +import SwiftJava // TODO: We should be able to autogenerate this as an extension based on // knowing that JavaClass was defined elsewhere. diff --git a/Sources/JavaKitReflection/Method+Utilities.swift b/Sources/JavaStdlib/JavaLangReflect/Method+Utilities.swift similarity index 68% rename from Sources/JavaKitReflection/Method+Utilities.swift rename to Sources/JavaStdlib/JavaLangReflect/Method+Utilities.swift index 71ee864ce..00ca3d6bf 100644 --- a/Sources/JavaKitReflection/Method+Utilities.swift +++ b/Sources/JavaStdlib/JavaLangReflect/Method+Utilities.swift @@ -13,23 +13,19 @@ //===----------------------------------------------------------------------===// extension Method { - /// Whether this is a 'public' method. - public var isPublic: Bool { - return (getModifiers() & 1) != 0 - } - - /// Whether this is a 'protected' method. - public var isProtected: Bool { - return (getModifiers() & 4) != 0 - } /// Whether this is a 'static' method. public var isStatic: Bool { - return (getModifiers() & 0x08) != 0 + return (getModifiers() & 0x00000008) != 0 } /// Whether this is a 'native' method. public var isNative: Bool { - return (getModifiers() & 256) != 0 + return (getModifiers() & 0x00000100) != 0 + } + + /// Whether this is a 'final' method. + public var isFinal: Bool { + return (getModifiers() & 0x00000010) != 0 } } diff --git a/Sources/JavaStdlib/JavaLangReflect/TypeVariable+Extensions.swift b/Sources/JavaStdlib/JavaLangReflect/TypeVariable+Extensions.swift new file mode 100644 index 000000000..157ae353d --- /dev/null +++ b/Sources/JavaStdlib/JavaLangReflect/TypeVariable+Extensions.swift @@ -0,0 +1,63 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import CSwiftJavaJNI +import SwiftJava + +// FIXME: all interfaces should ahve these https://github.com/swiftlang/swift-java/issues/430 +extension TypeVariable { + + @JavaMethod + public func toString() -> String + + @JavaMethod + public func getClass() -> JavaClass! + + @JavaMethod + public func equals(_ arg0: JavaObject?) -> Bool + + @JavaMethod + public func hashCode() -> Int32 + +} + +// FIXME: All Java objects are Hashable, we should handle that accordingly. +extension TypeVariable: Hashable { + + public func hash(into hasher: inout Hasher) { + guard let pojo = self.as(JavaObject.self) else { + return + } + + hasher.combine(pojo.hashCode()) + } + + public static func == (lhs: TypeVariable, rhs: TypeVariable) -> Bool { + guard let lhpojo: JavaObject = lhs.as(JavaObject.self) else { + return false + } + guard let rhpojo: JavaObject = rhs.as(JavaObject.self) else { + return false + } + + return lhpojo.equals(rhpojo) + } + +} + +extension TypeVariable { + public var description: String { + toString() + } +} \ No newline at end of file diff --git a/Sources/JavaKitReflection/generated/AccessibleObject.swift b/Sources/JavaStdlib/JavaLangReflect/generated/AccessibleObject.swift similarity index 96% rename from Sources/JavaKitReflection/generated/AccessibleObject.swift rename to Sources/JavaStdlib/JavaLangReflect/generated/AccessibleObject.swift index 84dcc4c23..c6b0f4fcc 100644 --- a/Sources/JavaKitReflection/generated/AccessibleObject.swift +++ b/Sources/JavaStdlib/JavaLangReflect/generated/AccessibleObject.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaClass("java.lang.reflect.AccessibleObject") open class AccessibleObject: JavaObject { diff --git a/Sources/JavaKitReflection/generated/AnnotatedType.swift b/Sources/JavaStdlib/JavaLangReflect/generated/AnnotatedType.swift similarity index 95% rename from Sources/JavaKitReflection/generated/AnnotatedType.swift rename to Sources/JavaStdlib/JavaLangReflect/generated/AnnotatedType.swift index ca9b9994e..87480ef4b 100644 --- a/Sources/JavaKitReflection/generated/AnnotatedType.swift +++ b/Sources/JavaStdlib/JavaLangReflect/generated/AnnotatedType.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.lang.reflect.AnnotatedType") public struct AnnotatedType { diff --git a/Sources/JavaKitReflection/generated/Annotation.swift b/Sources/JavaStdlib/JavaLangReflect/generated/Annotation.swift similarity index 90% rename from Sources/JavaKitReflection/generated/Annotation.swift rename to Sources/JavaStdlib/JavaLangReflect/generated/Annotation.swift index 269c0f9e1..1449bf329 100644 --- a/Sources/JavaKitReflection/generated/Annotation.swift +++ b/Sources/JavaStdlib/JavaLangReflect/generated/Annotation.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.lang.annotation.Annotation") public struct Annotation { diff --git a/Sources/JavaKitReflection/generated/Constructor.swift b/Sources/JavaStdlib/JavaLangReflect/generated/Constructor.swift similarity index 98% rename from Sources/JavaKitReflection/generated/Constructor.swift rename to Sources/JavaStdlib/JavaLangReflect/generated/Constructor.swift index 263d767d1..202cba9bd 100644 --- a/Sources/JavaKitReflection/generated/Constructor.swift +++ b/Sources/JavaStdlib/JavaLangReflect/generated/Constructor.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaClass("java.lang.reflect.Constructor") open class Constructor: Executable { diff --git a/Sources/JavaKitReflection/generated/Executable.swift b/Sources/JavaStdlib/JavaLangReflect/generated/Executable.swift similarity index 92% rename from Sources/JavaKitReflection/generated/Executable.swift rename to Sources/JavaStdlib/JavaLangReflect/generated/Executable.swift index 6a1f5b75c..af9931d35 100644 --- a/Sources/JavaKitReflection/generated/Executable.swift +++ b/Sources/JavaStdlib/JavaLangReflect/generated/Executable.swift @@ -1,7 +1,7 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaKitCollection -import JavaRuntime +import SwiftJava +import JavaUtil +import CSwiftJavaJNI @JavaClass("java.lang.reflect.Executable", implements: GenericDeclaration.self) open class Executable: AccessibleObject { @@ -11,9 +11,6 @@ open class Executable: AccessibleObject { @JavaMethod open func getModifiers() -> Int32 - @JavaMethod - open func getTypeParameters() -> [TypeVariable?] - @JavaMethod open func getParameterTypes() -> [JavaClass?] diff --git a/Sources/JavaKitReflection/generated/Field.swift b/Sources/JavaStdlib/JavaLangReflect/generated/Field.swift similarity index 97% rename from Sources/JavaKitReflection/generated/Field.swift rename to Sources/JavaStdlib/JavaLangReflect/generated/Field.swift index c67e56ffd..ba4f45383 100644 --- a/Sources/JavaKitReflection/generated/Field.swift +++ b/Sources/JavaStdlib/JavaLangReflect/generated/Field.swift @@ -1,7 +1,7 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaKitCollection -import JavaRuntime +import SwiftJava +import JavaUtil +import CSwiftJavaJNI @JavaClass("java.lang.reflect.Field") open class Field: AccessibleObject { diff --git a/Sources/JavaKitReflection/generated/GenericArrayType.swift b/Sources/JavaStdlib/JavaLangReflect/generated/GenericArrayType.swift similarity index 88% rename from Sources/JavaKitReflection/generated/GenericArrayType.swift rename to Sources/JavaStdlib/JavaLangReflect/generated/GenericArrayType.swift index 6f96a0d23..42375e086 100644 --- a/Sources/JavaKitReflection/generated/GenericArrayType.swift +++ b/Sources/JavaStdlib/JavaLangReflect/generated/GenericArrayType.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.lang.reflect.GenericArrayType", extends: Type.self) public struct GenericArrayType { diff --git a/Sources/JavaKitReflection/generated/GenericDeclaration.swift b/Sources/JavaStdlib/JavaLangReflect/generated/GenericDeclaration.swift similarity index 95% rename from Sources/JavaKitReflection/generated/GenericDeclaration.swift rename to Sources/JavaStdlib/JavaLangReflect/generated/GenericDeclaration.swift index 7a86bd8ac..839fa7dba 100644 --- a/Sources/JavaKitReflection/generated/GenericDeclaration.swift +++ b/Sources/JavaStdlib/JavaLangReflect/generated/GenericDeclaration.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.lang.reflect.GenericDeclaration") public struct GenericDeclaration { diff --git a/Sources/JavaKitReflection/generated/Method.swift b/Sources/JavaStdlib/JavaLangReflect/generated/Method.swift similarity index 98% rename from Sources/JavaKitReflection/generated/Method.swift rename to Sources/JavaStdlib/JavaLangReflect/generated/Method.swift index 46183f4f0..94371cd40 100644 --- a/Sources/JavaKitReflection/generated/Method.swift +++ b/Sources/JavaStdlib/JavaLangReflect/generated/Method.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaClass("java.lang.reflect.Method") open class Method: Executable { diff --git a/Sources/JavaKitReflection/generated/Parameter.swift b/Sources/JavaStdlib/JavaLangReflect/generated/Parameter.swift similarity index 95% rename from Sources/JavaKitReflection/generated/Parameter.swift rename to Sources/JavaStdlib/JavaLangReflect/generated/Parameter.swift index 30f2d6282..35ea70984 100644 --- a/Sources/JavaKitReflection/generated/Parameter.swift +++ b/Sources/JavaStdlib/JavaLangReflect/generated/Parameter.swift @@ -1,7 +1,7 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaKitCollection -import JavaRuntime +import SwiftJava +import JavaUtil +import CSwiftJavaJNI @JavaClass("java.lang.reflect.Parameter") open class Parameter: JavaObject { diff --git a/Sources/JavaStdlib/JavaLangReflect/generated/ParameterizedType.swift b/Sources/JavaStdlib/JavaLangReflect/generated/ParameterizedType.swift new file mode 100644 index 000000000..984c2b16e --- /dev/null +++ b/Sources/JavaStdlib/JavaLangReflect/generated/ParameterizedType.swift @@ -0,0 +1,39 @@ +// Auto-generated by Java-to-Swift wrapper generator. +import SwiftJava +import CSwiftJavaJNI + +@JavaInterface("java.lang.reflect.ParameterizedType", extends: Type.self) +public struct ParameterizedType { + @JavaMethod + public func getOwnerType() -> Type! + + @JavaMethod + public func getRawType() -> Type! + + @JavaMethod + public func getActualTypeArguments() -> [Type?] + + @JavaMethod + public func getTypeName() -> String +} + +extension ParameterizedType { + + @JavaMethod + public func toString() -> String + + @JavaMethod + public func getClass() -> JavaClass! + + @JavaMethod + public func equals(_ arg0: JavaObject?) -> Bool + + @JavaMethod + public func hashCode() -> Int32 +} + +extension ParameterizedType: CustomStringConvertible { + public var description: String { + toString() + } +} \ No newline at end of file diff --git a/Sources/JavaStdlib/JavaLangReflect/generated/Type.swift b/Sources/JavaStdlib/JavaLangReflect/generated/Type.swift new file mode 100644 index 000000000..2e85c3842 --- /dev/null +++ b/Sources/JavaStdlib/JavaLangReflect/generated/Type.swift @@ -0,0 +1,18 @@ +// Auto-generated by Java-to-Swift wrapper generator. +import SwiftJava +import CSwiftJavaJNI + +@JavaInterface("java.lang.reflect.Type") +public struct Type { + @JavaMethod + public func getTypeName() -> String + + @JavaMethod + public func toString() -> String +} + +extension Type: CustomStringConvertible { + public var description: String { + "JavaLangReflect.Type(\(self.toString()))" + } +} diff --git a/Sources/JavaKitReflection/generated/TypeVariable.swift b/Sources/JavaStdlib/JavaLangReflect/generated/TypeVariable.swift similarity index 90% rename from Sources/JavaKitReflection/generated/TypeVariable.swift rename to Sources/JavaStdlib/JavaLangReflect/generated/TypeVariable.swift index c925cdcf3..7bf8f7ba2 100644 --- a/Sources/JavaKitReflection/generated/TypeVariable.swift +++ b/Sources/JavaStdlib/JavaLangReflect/generated/TypeVariable.swift @@ -1,12 +1,12 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.lang.reflect.TypeVariable", extends: Type.self) -public struct TypeVariable { +public struct TypeVariable: CustomStringConvertible { @JavaMethod public func getGenericDeclaration() -> GenericDeclaration! - + @JavaMethod public func getAnnotatedBounds() -> [AnnotatedType?] diff --git a/Sources/JavaKitReflection/generated/WildcardType.swift b/Sources/JavaStdlib/JavaLangReflect/generated/WildcardType.swift similarity index 89% rename from Sources/JavaKitReflection/generated/WildcardType.swift rename to Sources/JavaStdlib/JavaLangReflect/generated/WildcardType.swift index e1551f506..a09b1b3b9 100644 --- a/Sources/JavaKitReflection/generated/WildcardType.swift +++ b/Sources/JavaStdlib/JavaLangReflect/generated/WildcardType.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.lang.reflect.WildcardType", extends: Type.self) public struct WildcardType { diff --git a/Sources/JavaKitReflection/swift-java.config b/Sources/JavaStdlib/JavaLangReflect/swift-java.config similarity index 100% rename from Sources/JavaKitReflection/swift-java.config rename to Sources/JavaStdlib/JavaLangReflect/swift-java.config diff --git a/Sources/JavaStdlib/JavaNet/URL+Extensions.swift b/Sources/JavaStdlib/JavaNet/URL+Extensions.swift new file mode 100644 index 000000000..2b220d1c8 --- /dev/null +++ b/Sources/JavaStdlib/JavaNet/URL+Extensions.swift @@ -0,0 +1,34 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftJava +import CSwiftJavaJNI + +import Foundation +public typealias SwiftJavaFoundationURL = Foundation.URL + +extension SwiftJavaFoundationURL { + public static func fromJava(_ url: URL) throws -> SwiftJavaFoundationURL { + guard let converted = SwiftJavaFoundationURL(string: try url.toURI().toString()) else { + throw SwiftJavaConversionError("Failed to convert \(URL.self) to \(SwiftJavaFoundationURL.self)") + } + return converted + } +} + +extension URL { + public static func fromSwift(_ url: SwiftJavaFoundationURL) throws -> URL { + return try URL(url.absoluteString) + } +} \ No newline at end of file diff --git a/Sources/JavaStdlib/JavaNet/URLClassLoader+Workaround.swift b/Sources/JavaStdlib/JavaNet/URLClassLoader+Workaround.swift new file mode 100644 index 000000000..4c55f6af6 --- /dev/null +++ b/Sources/JavaStdlib/JavaNet/URLClassLoader+Workaround.swift @@ -0,0 +1,22 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import CSwiftJavaJNI +import SwiftJava + +// FIXME: workaround until importing properly would make UCL inherit from CL https://github.com/swiftlang/swift-java/issues/423 +extension URLClassLoader /* workaround for missing inherits from ClassLoader */ { + @JavaMethod + public func loadClass(_ name: String) throws -> JavaClass? +} diff --git a/Sources/JavaKitNetwork/generated/URI.swift b/Sources/JavaStdlib/JavaNet/generated/URI.swift similarity index 98% rename from Sources/JavaKitNetwork/generated/URI.swift rename to Sources/JavaStdlib/JavaNet/generated/URI.swift index 000e29caa..d62424065 100644 --- a/Sources/JavaKitNetwork/generated/URI.swift +++ b/Sources/JavaStdlib/JavaNet/generated/URI.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaClass("java.net.URI") open class URI: JavaObject { diff --git a/Sources/JavaKitNetwork/generated/URL.swift b/Sources/JavaStdlib/JavaNet/generated/URL.swift similarity index 97% rename from Sources/JavaKitNetwork/generated/URL.swift rename to Sources/JavaStdlib/JavaNet/generated/URL.swift index ed0dee389..95ac8fb4c 100644 --- a/Sources/JavaKitNetwork/generated/URL.swift +++ b/Sources/JavaStdlib/JavaNet/generated/URL.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaClass("java.net.URL") open class URL: JavaObject { diff --git a/Sources/JavaKitNetwork/generated/URLClassLoader.swift b/Sources/JavaStdlib/JavaNet/generated/URLClassLoader.swift similarity index 93% rename from Sources/JavaKitNetwork/generated/URLClassLoader.swift rename to Sources/JavaStdlib/JavaNet/generated/URLClassLoader.swift index eac9c14c0..d16e2eac6 100644 --- a/Sources/JavaKitNetwork/generated/URLClassLoader.swift +++ b/Sources/JavaStdlib/JavaNet/generated/URLClassLoader.swift @@ -1,7 +1,7 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaKitCollection -import JavaRuntime +import SwiftJava +import JavaUtil +import CSwiftJavaJNI @JavaClass("java.net.URLClassLoader") open class URLClassLoader: JavaObject { diff --git a/Sources/JavaKitNetwork/generated/URLConnection.swift b/Sources/JavaStdlib/JavaNet/generated/URLConnection.swift similarity index 98% rename from Sources/JavaKitNetwork/generated/URLConnection.swift rename to Sources/JavaStdlib/JavaNet/generated/URLConnection.swift index 94fd6f523..332e7425c 100644 --- a/Sources/JavaKitNetwork/generated/URLConnection.swift +++ b/Sources/JavaStdlib/JavaNet/generated/URLConnection.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaClass("java.net.URLConnection") public struct URLConnection { diff --git a/Sources/JavaKitNetwork/swift-java.config b/Sources/JavaStdlib/JavaNet/swift-java.config similarity index 100% rename from Sources/JavaKitNetwork/swift-java.config rename to Sources/JavaStdlib/JavaNet/swift-java.config diff --git a/Sources/JavaKitCollection/JavaEnumeration+Sequence.swift b/Sources/JavaStdlib/JavaUtil/JavaEnumeration+Sequence.swift similarity index 100% rename from Sources/JavaKitCollection/JavaEnumeration+Sequence.swift rename to Sources/JavaStdlib/JavaUtil/JavaEnumeration+Sequence.swift diff --git a/Sources/JavaKitCollection/JavaIterator+Iterator.swift b/Sources/JavaStdlib/JavaUtil/JavaIterator+Iterator.swift similarity index 97% rename from Sources/JavaKitCollection/JavaIterator+Iterator.swift rename to Sources/JavaStdlib/JavaUtil/JavaIterator+Iterator.swift index add2a48b4..ad4275270 100644 --- a/Sources/JavaKitCollection/JavaIterator+Iterator.swift +++ b/Sources/JavaStdlib/JavaUtil/JavaIterator+Iterator.swift @@ -11,7 +11,7 @@ // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// -import JavaKit +import SwiftJava extension JavaIterator: IteratorProtocol { public typealias Element = E diff --git a/Sources/JavaKitCollection/List+Sequence.swift b/Sources/JavaStdlib/JavaUtil/List+Sequence.swift similarity index 100% rename from Sources/JavaKitCollection/List+Sequence.swift rename to Sources/JavaStdlib/JavaUtil/List+Sequence.swift diff --git a/Sources/JavaKitCollection/generated/ArrayDeque.swift b/Sources/JavaStdlib/JavaUtil/generated/ArrayDeque.swift similarity index 98% rename from Sources/JavaKitCollection/generated/ArrayDeque.swift rename to Sources/JavaStdlib/JavaUtil/generated/ArrayDeque.swift index 79a1e20bf..b8a1fb189 100644 --- a/Sources/JavaKitCollection/generated/ArrayDeque.swift +++ b/Sources/JavaStdlib/JavaUtil/generated/ArrayDeque.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaClass("java.util.ArrayDeque") open class ArrayDeque: JavaObject { diff --git a/Sources/JavaKitCollection/generated/ArrayList.swift b/Sources/JavaStdlib/JavaUtil/generated/ArrayList.swift similarity index 98% rename from Sources/JavaKitCollection/generated/ArrayList.swift rename to Sources/JavaStdlib/JavaUtil/generated/ArrayList.swift index 6748640b3..9ead8f4c0 100644 --- a/Sources/JavaKitCollection/generated/ArrayList.swift +++ b/Sources/JavaStdlib/JavaUtil/generated/ArrayList.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaClass("java.util.ArrayList", implements: List.self, RandomAccess.self) open class ArrayList: JavaObject { diff --git a/Sources/JavaKitCollection/generated/BitSet.swift b/Sources/JavaStdlib/JavaUtil/generated/BitSet.swift similarity index 98% rename from Sources/JavaKitCollection/generated/BitSet.swift rename to Sources/JavaStdlib/JavaUtil/generated/BitSet.swift index d5211c28b..7356b4b89 100644 --- a/Sources/JavaKitCollection/generated/BitSet.swift +++ b/Sources/JavaStdlib/JavaUtil/generated/BitSet.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaClass("java.util.BitSet") open class BitSet: JavaObject { diff --git a/Sources/JavaKitCollection/generated/Enumeration.swift b/Sources/JavaStdlib/JavaUtil/generated/Enumeration.swift similarity index 89% rename from Sources/JavaKitCollection/generated/Enumeration.swift rename to Sources/JavaStdlib/JavaUtil/generated/Enumeration.swift index 492cc3b2a..9c039b166 100644 --- a/Sources/JavaKitCollection/generated/Enumeration.swift +++ b/Sources/JavaStdlib/JavaUtil/generated/Enumeration.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.util.Enumeration") public struct Enumeration { diff --git a/Sources/JavaKitCollection/generated/HashMap.swift b/Sources/JavaStdlib/JavaUtil/generated/HashMap.swift similarity index 97% rename from Sources/JavaKitCollection/generated/HashMap.swift rename to Sources/JavaStdlib/JavaUtil/generated/HashMap.swift index 424dfbb9a..a217d48fd 100644 --- a/Sources/JavaKitCollection/generated/HashMap.swift +++ b/Sources/JavaStdlib/JavaUtil/generated/HashMap.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaClass("java.util.HashMap") open class HashMap: JavaObject { diff --git a/Sources/JavaKitCollection/generated/HashSet.swift b/Sources/JavaStdlib/JavaUtil/generated/HashSet.swift similarity index 97% rename from Sources/JavaKitCollection/generated/HashSet.swift rename to Sources/JavaStdlib/JavaUtil/generated/HashSet.swift index 97b15dffe..3b089c861 100644 --- a/Sources/JavaKitCollection/generated/HashSet.swift +++ b/Sources/JavaStdlib/JavaUtil/generated/HashSet.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaClass("java.util.HashSet", implements: JavaSet.self) open class HashSet: JavaObject { diff --git a/Sources/JavaKitCollection/generated/JavaCollection.swift b/Sources/JavaStdlib/JavaUtil/generated/JavaCollection.swift similarity index 96% rename from Sources/JavaKitCollection/generated/JavaCollection.swift rename to Sources/JavaStdlib/JavaUtil/generated/JavaCollection.swift index 26ca6827e..3a8db21b3 100644 --- a/Sources/JavaKitCollection/generated/JavaCollection.swift +++ b/Sources/JavaStdlib/JavaUtil/generated/JavaCollection.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.util.Collection") public struct JavaCollection { diff --git a/Sources/JavaKitCollection/generated/JavaDictionary.swift b/Sources/JavaStdlib/JavaUtil/generated/JavaDictionary.swift similarity index 94% rename from Sources/JavaKitCollection/generated/JavaDictionary.swift rename to Sources/JavaStdlib/JavaUtil/generated/JavaDictionary.swift index 43ded9154..0a6508f5c 100644 --- a/Sources/JavaKitCollection/generated/JavaDictionary.swift +++ b/Sources/JavaStdlib/JavaUtil/generated/JavaDictionary.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaClass("java.util.Dictionary") open class JavaDictionary: JavaObject { diff --git a/Sources/JavaKitCollection/generated/JavaIterator.swift b/Sources/JavaStdlib/JavaUtil/generated/JavaIterator.swift similarity index 87% rename from Sources/JavaKitCollection/generated/JavaIterator.swift rename to Sources/JavaStdlib/JavaUtil/generated/JavaIterator.swift index 382c44e53..06f09d7f0 100644 --- a/Sources/JavaKitCollection/generated/JavaIterator.swift +++ b/Sources/JavaStdlib/JavaUtil/generated/JavaIterator.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.util.Iterator") public struct JavaIterator { diff --git a/Sources/JavaKitCollection/generated/JavaSet.swift b/Sources/JavaStdlib/JavaUtil/generated/JavaSet.swift similarity index 99% rename from Sources/JavaKitCollection/generated/JavaSet.swift rename to Sources/JavaStdlib/JavaUtil/generated/JavaSet.swift index d924d2d17..12ff00567 100644 --- a/Sources/JavaKitCollection/generated/JavaSet.swift +++ b/Sources/JavaStdlib/JavaUtil/generated/JavaSet.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.util.Set", extends: JavaCollection.self) public struct JavaSet { diff --git a/Sources/JavaKitCollection/generated/LinkedList.swift b/Sources/JavaStdlib/JavaUtil/generated/LinkedList.swift similarity index 98% rename from Sources/JavaKitCollection/generated/LinkedList.swift rename to Sources/JavaStdlib/JavaUtil/generated/LinkedList.swift index 25abfbbf0..2464980f0 100644 --- a/Sources/JavaKitCollection/generated/LinkedList.swift +++ b/Sources/JavaStdlib/JavaUtil/generated/LinkedList.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaClass("java.util.LinkedList") public struct LinkedList { diff --git a/Sources/JavaKitCollection/generated/List.swift b/Sources/JavaStdlib/JavaUtil/generated/List.swift similarity index 99% rename from Sources/JavaKitCollection/generated/List.swift rename to Sources/JavaStdlib/JavaUtil/generated/List.swift index b264f88eb..2ebc19420 100644 --- a/Sources/JavaKitCollection/generated/List.swift +++ b/Sources/JavaStdlib/JavaUtil/generated/List.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.util.List") public struct List { diff --git a/Sources/JavaKitCollection/generated/ListIterator.swift b/Sources/JavaStdlib/JavaUtil/generated/ListIterator.swift similarity index 94% rename from Sources/JavaKitCollection/generated/ListIterator.swift rename to Sources/JavaStdlib/JavaUtil/generated/ListIterator.swift index 7b2731605..121330b13 100644 --- a/Sources/JavaKitCollection/generated/ListIterator.swift +++ b/Sources/JavaStdlib/JavaUtil/generated/ListIterator.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.util.ListIterator", extends: JavaIterator.self) public struct ListIterator { diff --git a/Sources/JavaKitCollection/generated/PriorityQueue.swift b/Sources/JavaStdlib/JavaUtil/generated/PriorityQueue.swift similarity index 97% rename from Sources/JavaKitCollection/generated/PriorityQueue.swift rename to Sources/JavaStdlib/JavaUtil/generated/PriorityQueue.swift index 14e50f31b..d9d9a85a5 100644 --- a/Sources/JavaKitCollection/generated/PriorityQueue.swift +++ b/Sources/JavaStdlib/JavaUtil/generated/PriorityQueue.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaClass("java.util.PriorityQueue") open class PriorityQueue: JavaObject { diff --git a/Sources/JavaKitCollection/generated/Queue.swift b/Sources/JavaStdlib/JavaUtil/generated/Queue.swift similarity index 97% rename from Sources/JavaKitCollection/generated/Queue.swift rename to Sources/JavaStdlib/JavaUtil/generated/Queue.swift index 44373fce8..b007e90a7 100644 --- a/Sources/JavaKitCollection/generated/Queue.swift +++ b/Sources/JavaStdlib/JavaUtil/generated/Queue.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.util.Queue", extends: JavaCollection.self) public struct Queue { diff --git a/Sources/JavaKitCollection/generated/RandomAccess.swift b/Sources/JavaStdlib/JavaUtil/generated/RandomAccess.swift similarity index 77% rename from Sources/JavaKitCollection/generated/RandomAccess.swift rename to Sources/JavaStdlib/JavaUtil/generated/RandomAccess.swift index 0084ceede..91b3fa31c 100644 --- a/Sources/JavaKitCollection/generated/RandomAccess.swift +++ b/Sources/JavaStdlib/JavaUtil/generated/RandomAccess.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.util.RandomAccess") public struct RandomAccess { diff --git a/Sources/JavaKitCollection/generated/Stack.swift b/Sources/JavaStdlib/JavaUtil/generated/Stack.swift similarity index 92% rename from Sources/JavaKitCollection/generated/Stack.swift rename to Sources/JavaStdlib/JavaUtil/generated/Stack.swift index 867e9d443..be4330ebf 100644 --- a/Sources/JavaKitCollection/generated/Stack.swift +++ b/Sources/JavaStdlib/JavaUtil/generated/Stack.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaClass("java.util.Stack") open class Stack: JavaObject { diff --git a/Sources/JavaKitCollection/generated/TreeMap.swift b/Sources/JavaStdlib/JavaUtil/generated/TreeMap.swift similarity index 98% rename from Sources/JavaKitCollection/generated/TreeMap.swift rename to Sources/JavaStdlib/JavaUtil/generated/TreeMap.swift index 7796d555e..c6dd3bb05 100644 --- a/Sources/JavaKitCollection/generated/TreeMap.swift +++ b/Sources/JavaStdlib/JavaUtil/generated/TreeMap.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaClass("java.util.TreeMap") open class TreeMap: JavaObject { diff --git a/Sources/JavaKitCollection/generated/TreeSet.swift b/Sources/JavaStdlib/JavaUtil/generated/TreeSet.swift similarity index 97% rename from Sources/JavaKitCollection/generated/TreeSet.swift rename to Sources/JavaStdlib/JavaUtil/generated/TreeSet.swift index 7e1807abb..3ec9b1ea7 100644 --- a/Sources/JavaKitCollection/generated/TreeSet.swift +++ b/Sources/JavaStdlib/JavaUtil/generated/TreeSet.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaClass("java.util.TreeSet") open class TreeSet: JavaObject { diff --git a/Sources/JavaKitCollection/swift-java.config b/Sources/JavaStdlib/JavaUtil/swift-java.config similarity index 100% rename from Sources/JavaKitCollection/swift-java.config rename to Sources/JavaStdlib/JavaUtil/swift-java.config diff --git a/Sources/JavaKitFunction/generated/JavaBiConsumer.swift b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaBiConsumer.swift similarity index 91% rename from Sources/JavaKitFunction/generated/JavaBiConsumer.swift rename to Sources/JavaStdlib/JavaUtilFunction/generated/JavaBiConsumer.swift index c5f5b7bfa..a01a0933e 100644 --- a/Sources/JavaKitFunction/generated/JavaBiConsumer.swift +++ b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaBiConsumer.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.util.function.BiConsumer") public struct JavaBiConsumer { diff --git a/Sources/JavaKitFunction/generated/JavaBiFunction.swift b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaBiFunction.swift similarity index 91% rename from Sources/JavaKitFunction/generated/JavaBiFunction.swift rename to Sources/JavaStdlib/JavaUtilFunction/generated/JavaBiFunction.swift index edecbff58..ad54c58b1 100644 --- a/Sources/JavaKitFunction/generated/JavaBiFunction.swift +++ b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaBiFunction.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.util.function.BiFunction") public struct JavaBiFunction { diff --git a/Sources/JavaKitFunction/generated/JavaBiPredicate.swift b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaBiPredicate.swift similarity index 94% rename from Sources/JavaKitFunction/generated/JavaBiPredicate.swift rename to Sources/JavaStdlib/JavaUtilFunction/generated/JavaBiPredicate.swift index 5fbc145ef..ee8aa0f50 100644 --- a/Sources/JavaKitFunction/generated/JavaBiPredicate.swift +++ b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaBiPredicate.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.util.function.BiPredicate") public struct JavaBiPredicate { diff --git a/Sources/JavaKitFunction/generated/JavaBinaryOperator.swift b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaBinaryOperator.swift similarity index 92% rename from Sources/JavaKitFunction/generated/JavaBinaryOperator.swift rename to Sources/JavaStdlib/JavaUtilFunction/generated/JavaBinaryOperator.swift index 3d0b0cd5d..1c7f54569 100644 --- a/Sources/JavaKitFunction/generated/JavaBinaryOperator.swift +++ b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaBinaryOperator.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface( "java.util.function.BinaryOperator", diff --git a/Sources/JavaKitFunction/generated/JavaBooleanSupplier.swift b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaBooleanSupplier.swift similarity index 83% rename from Sources/JavaKitFunction/generated/JavaBooleanSupplier.swift rename to Sources/JavaStdlib/JavaUtilFunction/generated/JavaBooleanSupplier.swift index eef960252..e6e4a3ced 100644 --- a/Sources/JavaKitFunction/generated/JavaBooleanSupplier.swift +++ b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaBooleanSupplier.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.util.function.BooleanSupplier") public struct JavaBooleanSupplier { diff --git a/Sources/JavaKitFunction/generated/JavaConsumer.swift b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaConsumer.swift similarity index 88% rename from Sources/JavaKitFunction/generated/JavaConsumer.swift rename to Sources/JavaStdlib/JavaUtilFunction/generated/JavaConsumer.swift index 91eac1063..7da9bea8d 100644 --- a/Sources/JavaKitFunction/generated/JavaConsumer.swift +++ b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaConsumer.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.util.function.Consumer") public struct JavaConsumer { diff --git a/Sources/JavaKitFunction/generated/JavaDoubleBinaryOperator.swift b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaDoubleBinaryOperator.swift similarity index 86% rename from Sources/JavaKitFunction/generated/JavaDoubleBinaryOperator.swift rename to Sources/JavaStdlib/JavaUtilFunction/generated/JavaDoubleBinaryOperator.swift index 0415ed90e..f6c1c671e 100644 --- a/Sources/JavaKitFunction/generated/JavaDoubleBinaryOperator.swift +++ b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaDoubleBinaryOperator.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.util.function.DoubleBinaryOperator") public struct JavaDoubleBinaryOperator { diff --git a/Sources/JavaKitFunction/generated/JavaDoubleConsumer.swift b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaDoubleConsumer.swift similarity index 88% rename from Sources/JavaKitFunction/generated/JavaDoubleConsumer.swift rename to Sources/JavaStdlib/JavaUtilFunction/generated/JavaDoubleConsumer.swift index 9c3225847..baf53c4ac 100644 --- a/Sources/JavaKitFunction/generated/JavaDoubleConsumer.swift +++ b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaDoubleConsumer.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.util.function.DoubleConsumer") public struct JavaDoubleConsumer { diff --git a/Sources/JavaKitFunction/generated/JavaDoubleFunction.swift b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaDoubleFunction.swift similarity index 85% rename from Sources/JavaKitFunction/generated/JavaDoubleFunction.swift rename to Sources/JavaStdlib/JavaUtilFunction/generated/JavaDoubleFunction.swift index 4ff4f3ef9..6f5d6752f 100644 --- a/Sources/JavaKitFunction/generated/JavaDoubleFunction.swift +++ b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaDoubleFunction.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.util.function.DoubleFunction") public struct JavaDoubleFunction { diff --git a/Sources/JavaKitFunction/generated/JavaDoublePredicate.swift b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaDoublePredicate.swift similarity index 92% rename from Sources/JavaKitFunction/generated/JavaDoublePredicate.swift rename to Sources/JavaStdlib/JavaUtilFunction/generated/JavaDoublePredicate.swift index b2add42f1..c594518d1 100644 --- a/Sources/JavaKitFunction/generated/JavaDoublePredicate.swift +++ b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaDoublePredicate.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.util.function.DoublePredicate") public struct JavaDoublePredicate { diff --git a/Sources/JavaKitFunction/generated/JavaDoubleSupplier.swift b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaDoubleSupplier.swift similarity index 83% rename from Sources/JavaKitFunction/generated/JavaDoubleSupplier.swift rename to Sources/JavaStdlib/JavaUtilFunction/generated/JavaDoubleSupplier.swift index 184a4353c..839ae7e78 100644 --- a/Sources/JavaKitFunction/generated/JavaDoubleSupplier.swift +++ b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaDoubleSupplier.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.util.function.DoubleSupplier") public struct JavaDoubleSupplier { diff --git a/Sources/JavaKitFunction/generated/JavaDoubleToIntFunction.swift b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaDoubleToIntFunction.swift similarity index 85% rename from Sources/JavaKitFunction/generated/JavaDoubleToIntFunction.swift rename to Sources/JavaStdlib/JavaUtilFunction/generated/JavaDoubleToIntFunction.swift index ae4ab0b3c..438249ebf 100644 --- a/Sources/JavaKitFunction/generated/JavaDoubleToIntFunction.swift +++ b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaDoubleToIntFunction.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.util.function.DoubleToIntFunction") public struct JavaDoubleToIntFunction { diff --git a/Sources/JavaKitFunction/generated/JavaDoubleToLongFunction.swift b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaDoubleToLongFunction.swift similarity index 85% rename from Sources/JavaKitFunction/generated/JavaDoubleToLongFunction.swift rename to Sources/JavaStdlib/JavaUtilFunction/generated/JavaDoubleToLongFunction.swift index b0c9f9d62..76b916db4 100644 --- a/Sources/JavaKitFunction/generated/JavaDoubleToLongFunction.swift +++ b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaDoubleToLongFunction.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.util.function.DoubleToLongFunction") public struct JavaDoubleToLongFunction { diff --git a/Sources/JavaKitFunction/generated/JavaDoubleUnaryOperator.swift b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaDoubleUnaryOperator.swift similarity index 93% rename from Sources/JavaKitFunction/generated/JavaDoubleUnaryOperator.swift rename to Sources/JavaStdlib/JavaUtilFunction/generated/JavaDoubleUnaryOperator.swift index eb29139a2..cf1ff7e5c 100644 --- a/Sources/JavaKitFunction/generated/JavaDoubleUnaryOperator.swift +++ b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaDoubleUnaryOperator.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.util.function.DoubleUnaryOperator") public struct JavaDoubleUnaryOperator { diff --git a/Sources/JavaKitFunction/generated/JavaFunction.swift b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaFunction.swift similarity index 94% rename from Sources/JavaKitFunction/generated/JavaFunction.swift rename to Sources/JavaStdlib/JavaUtilFunction/generated/JavaFunction.swift index 744461a96..7a8165d87 100644 --- a/Sources/JavaKitFunction/generated/JavaFunction.swift +++ b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaFunction.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.util.function.Function") public struct JavaFunction { diff --git a/Sources/JavaKitFunction/generated/JavaIntBinaryOperator.swift b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaIntBinaryOperator.swift similarity index 85% rename from Sources/JavaKitFunction/generated/JavaIntBinaryOperator.swift rename to Sources/JavaStdlib/JavaUtilFunction/generated/JavaIntBinaryOperator.swift index 458885fe5..3df580c76 100644 --- a/Sources/JavaKitFunction/generated/JavaIntBinaryOperator.swift +++ b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaIntBinaryOperator.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.util.function.IntBinaryOperator") public struct JavaIntBinaryOperator { diff --git a/Sources/JavaKitFunction/generated/JavaIntConsumer.swift b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaIntConsumer.swift similarity index 87% rename from Sources/JavaKitFunction/generated/JavaIntConsumer.swift rename to Sources/JavaStdlib/JavaUtilFunction/generated/JavaIntConsumer.swift index 704a45138..a8ac7c0bf 100644 --- a/Sources/JavaKitFunction/generated/JavaIntConsumer.swift +++ b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaIntConsumer.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.util.function.IntConsumer") public struct JavaIntConsumer { diff --git a/Sources/JavaKitFunction/generated/JavaIntFunction.swift b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaIntFunction.swift similarity index 85% rename from Sources/JavaKitFunction/generated/JavaIntFunction.swift rename to Sources/JavaStdlib/JavaUtilFunction/generated/JavaIntFunction.swift index cd7e7219a..6ebb7292d 100644 --- a/Sources/JavaKitFunction/generated/JavaIntFunction.swift +++ b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaIntFunction.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.util.function.IntFunction") public struct JavaIntFunction { diff --git a/Sources/JavaKitFunction/generated/JavaIntPredicate.swift b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaIntPredicate.swift similarity index 91% rename from Sources/JavaKitFunction/generated/JavaIntPredicate.swift rename to Sources/JavaStdlib/JavaUtilFunction/generated/JavaIntPredicate.swift index 9580ef874..66c5e1335 100644 --- a/Sources/JavaKitFunction/generated/JavaIntPredicate.swift +++ b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaIntPredicate.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.util.function.IntPredicate") public struct JavaIntPredicate { diff --git a/Sources/JavaKitFunction/generated/JavaIntSupplier.swift b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaIntSupplier.swift similarity index 83% rename from Sources/JavaKitFunction/generated/JavaIntSupplier.swift rename to Sources/JavaStdlib/JavaUtilFunction/generated/JavaIntSupplier.swift index f0fae86aa..0976fd53d 100644 --- a/Sources/JavaKitFunction/generated/JavaIntSupplier.swift +++ b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaIntSupplier.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.util.function.IntSupplier") public struct JavaIntSupplier { diff --git a/Sources/JavaKitFunction/generated/JavaIntToDoubleFunction.swift b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaIntToDoubleFunction.swift similarity index 85% rename from Sources/JavaKitFunction/generated/JavaIntToDoubleFunction.swift rename to Sources/JavaStdlib/JavaUtilFunction/generated/JavaIntToDoubleFunction.swift index 0a52e5b7e..9891e8153 100644 --- a/Sources/JavaKitFunction/generated/JavaIntToDoubleFunction.swift +++ b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaIntToDoubleFunction.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.util.function.IntToDoubleFunction") public struct JavaIntToDoubleFunction { diff --git a/Sources/JavaKitFunction/generated/JavaIntToLongFunction.swift b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaIntToLongFunction.swift similarity index 84% rename from Sources/JavaKitFunction/generated/JavaIntToLongFunction.swift rename to Sources/JavaStdlib/JavaUtilFunction/generated/JavaIntToLongFunction.swift index 395dc8761..17a7b7fac 100644 --- a/Sources/JavaKitFunction/generated/JavaIntToLongFunction.swift +++ b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaIntToLongFunction.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.util.function.IntToLongFunction") public struct JavaIntToLongFunction { diff --git a/Sources/JavaKitFunction/generated/JavaIntUnaryOperator.swift b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaIntUnaryOperator.swift similarity index 93% rename from Sources/JavaKitFunction/generated/JavaIntUnaryOperator.swift rename to Sources/JavaStdlib/JavaUtilFunction/generated/JavaIntUnaryOperator.swift index 221b3a97a..89528a2b8 100644 --- a/Sources/JavaKitFunction/generated/JavaIntUnaryOperator.swift +++ b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaIntUnaryOperator.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.util.function.IntUnaryOperator") public struct JavaIntUnaryOperator { diff --git a/Sources/JavaKitFunction/generated/JavaLongBinaryOperator.swift b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaLongBinaryOperator.swift similarity index 85% rename from Sources/JavaKitFunction/generated/JavaLongBinaryOperator.swift rename to Sources/JavaStdlib/JavaUtilFunction/generated/JavaLongBinaryOperator.swift index 88035edc4..1cd53b933 100644 --- a/Sources/JavaKitFunction/generated/JavaLongBinaryOperator.swift +++ b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaLongBinaryOperator.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.util.function.LongBinaryOperator") public struct JavaLongBinaryOperator { diff --git a/Sources/JavaKitFunction/generated/JavaLongConsumer.swift b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaLongConsumer.swift similarity index 87% rename from Sources/JavaKitFunction/generated/JavaLongConsumer.swift rename to Sources/JavaStdlib/JavaUtilFunction/generated/JavaLongConsumer.swift index b0ef6013b..4c84754a7 100644 --- a/Sources/JavaKitFunction/generated/JavaLongConsumer.swift +++ b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaLongConsumer.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.util.function.LongConsumer") public struct JavaLongConsumer { diff --git a/Sources/JavaKitFunction/generated/JavaLongFunction.swift b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaLongFunction.swift similarity index 85% rename from Sources/JavaKitFunction/generated/JavaLongFunction.swift rename to Sources/JavaStdlib/JavaUtilFunction/generated/JavaLongFunction.swift index 2d9a75597..9ce4cef15 100644 --- a/Sources/JavaKitFunction/generated/JavaLongFunction.swift +++ b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaLongFunction.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.util.function.LongFunction") public struct JavaLongFunction { diff --git a/Sources/JavaKitFunction/generated/JavaLongPredicate.swift b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaLongPredicate.swift similarity index 91% rename from Sources/JavaKitFunction/generated/JavaLongPredicate.swift rename to Sources/JavaStdlib/JavaUtilFunction/generated/JavaLongPredicate.swift index 3ed020eb6..8f8f91fcf 100644 --- a/Sources/JavaKitFunction/generated/JavaLongPredicate.swift +++ b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaLongPredicate.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.util.function.LongPredicate") public struct JavaLongPredicate { diff --git a/Sources/JavaKitFunction/generated/JavaLongSupplier.swift b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaLongSupplier.swift similarity index 83% rename from Sources/JavaKitFunction/generated/JavaLongSupplier.swift rename to Sources/JavaStdlib/JavaUtilFunction/generated/JavaLongSupplier.swift index 03ad63033..e0a203a18 100644 --- a/Sources/JavaKitFunction/generated/JavaLongSupplier.swift +++ b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaLongSupplier.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.util.function.LongSupplier") public struct JavaLongSupplier { diff --git a/Sources/JavaKitFunction/generated/JavaLongToDoubleFunction.swift b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaLongToDoubleFunction.swift similarity index 85% rename from Sources/JavaKitFunction/generated/JavaLongToDoubleFunction.swift rename to Sources/JavaStdlib/JavaUtilFunction/generated/JavaLongToDoubleFunction.swift index 99b56ba32..7167d8fc9 100644 --- a/Sources/JavaKitFunction/generated/JavaLongToDoubleFunction.swift +++ b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaLongToDoubleFunction.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.util.function.LongToDoubleFunction") public struct JavaLongToDoubleFunction { diff --git a/Sources/JavaKitFunction/generated/JavaLongToIntFunction.swift b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaLongToIntFunction.swift similarity index 84% rename from Sources/JavaKitFunction/generated/JavaLongToIntFunction.swift rename to Sources/JavaStdlib/JavaUtilFunction/generated/JavaLongToIntFunction.swift index 2c952a174..006713043 100644 --- a/Sources/JavaKitFunction/generated/JavaLongToIntFunction.swift +++ b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaLongToIntFunction.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.util.function.LongToIntFunction") public struct JavaLongToIntFunction { diff --git a/Sources/JavaKitFunction/generated/JavaLongUnaryOperator.swift b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaLongUnaryOperator.swift similarity index 93% rename from Sources/JavaKitFunction/generated/JavaLongUnaryOperator.swift rename to Sources/JavaStdlib/JavaUtilFunction/generated/JavaLongUnaryOperator.swift index fe99e3380..8cf8944be 100644 --- a/Sources/JavaKitFunction/generated/JavaLongUnaryOperator.swift +++ b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaLongUnaryOperator.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.util.function.LongUnaryOperator") public struct JavaLongUnaryOperator { diff --git a/Sources/JavaKitFunction/generated/JavaObjDoubleConsumer.swift b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaObjDoubleConsumer.swift similarity index 86% rename from Sources/JavaKitFunction/generated/JavaObjDoubleConsumer.swift rename to Sources/JavaStdlib/JavaUtilFunction/generated/JavaObjDoubleConsumer.swift index cf1ac236f..aa7d5a48a 100644 --- a/Sources/JavaKitFunction/generated/JavaObjDoubleConsumer.swift +++ b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaObjDoubleConsumer.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.util.function.ObjDoubleConsumer") public struct JavaObjDoubleConsumer { diff --git a/Sources/JavaKitFunction/generated/JavaObjIntConsumer.swift b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaObjIntConsumer.swift similarity index 85% rename from Sources/JavaKitFunction/generated/JavaObjIntConsumer.swift rename to Sources/JavaStdlib/JavaUtilFunction/generated/JavaObjIntConsumer.swift index 6c1e3b1ea..c53c3631c 100644 --- a/Sources/JavaKitFunction/generated/JavaObjIntConsumer.swift +++ b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaObjIntConsumer.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.util.function.ObjIntConsumer") public struct JavaObjIntConsumer { diff --git a/Sources/JavaKitFunction/generated/JavaObjLongConsumer.swift b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaObjLongConsumer.swift similarity index 86% rename from Sources/JavaKitFunction/generated/JavaObjLongConsumer.swift rename to Sources/JavaStdlib/JavaUtilFunction/generated/JavaObjLongConsumer.swift index 5486e9102..ff4f7798a 100644 --- a/Sources/JavaKitFunction/generated/JavaObjLongConsumer.swift +++ b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaObjLongConsumer.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.util.function.ObjLongConsumer") public struct JavaObjLongConsumer { diff --git a/Sources/JavaKitFunction/generated/JavaPredicate.swift b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaPredicate.swift similarity index 95% rename from Sources/JavaKitFunction/generated/JavaPredicate.swift rename to Sources/JavaStdlib/JavaUtilFunction/generated/JavaPredicate.swift index 9c953e3f6..b888e1786 100644 --- a/Sources/JavaKitFunction/generated/JavaPredicate.swift +++ b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaPredicate.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.util.function.Predicate") public struct JavaPredicate { diff --git a/Sources/JavaKitFunction/generated/JavaSupplier.swift b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaSupplier.swift similarity index 83% rename from Sources/JavaKitFunction/generated/JavaSupplier.swift rename to Sources/JavaStdlib/JavaUtilFunction/generated/JavaSupplier.swift index b44740dd8..4d4c73cda 100644 --- a/Sources/JavaKitFunction/generated/JavaSupplier.swift +++ b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaSupplier.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.util.function.Supplier") public struct JavaSupplier { diff --git a/Sources/JavaKitFunction/generated/JavaToDoubleBiFunction.swift b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaToDoubleBiFunction.swift similarity index 88% rename from Sources/JavaKitFunction/generated/JavaToDoubleBiFunction.swift rename to Sources/JavaStdlib/JavaUtilFunction/generated/JavaToDoubleBiFunction.swift index 22ccf62c3..35f77b785 100644 --- a/Sources/JavaKitFunction/generated/JavaToDoubleBiFunction.swift +++ b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaToDoubleBiFunction.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.util.function.ToDoubleBiFunction") public struct JavaToDoubleBiFunction { diff --git a/Sources/JavaKitFunction/generated/JavaToDoubleFunction.swift b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaToDoubleFunction.swift similarity index 86% rename from Sources/JavaKitFunction/generated/JavaToDoubleFunction.swift rename to Sources/JavaStdlib/JavaUtilFunction/generated/JavaToDoubleFunction.swift index bf1c1d37a..56ee180d9 100644 --- a/Sources/JavaKitFunction/generated/JavaToDoubleFunction.swift +++ b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaToDoubleFunction.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.util.function.ToDoubleFunction") public struct JavaToDoubleFunction { diff --git a/Sources/JavaKitFunction/generated/JavaToIntBiFunction.swift b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaToIntBiFunction.swift similarity index 87% rename from Sources/JavaKitFunction/generated/JavaToIntBiFunction.swift rename to Sources/JavaStdlib/JavaUtilFunction/generated/JavaToIntBiFunction.swift index 6ebc0ff18..dc17fa2b2 100644 --- a/Sources/JavaKitFunction/generated/JavaToIntBiFunction.swift +++ b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaToIntBiFunction.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.util.function.ToIntBiFunction") public struct JavaToIntBiFunction { diff --git a/Sources/JavaKitFunction/generated/JavaToIntFunction.swift b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaToIntFunction.swift similarity index 85% rename from Sources/JavaKitFunction/generated/JavaToIntFunction.swift rename to Sources/JavaStdlib/JavaUtilFunction/generated/JavaToIntFunction.swift index c960913d6..3663f4993 100644 --- a/Sources/JavaKitFunction/generated/JavaToIntFunction.swift +++ b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaToIntFunction.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.util.function.ToIntFunction") public struct JavaToIntFunction { diff --git a/Sources/JavaKitFunction/generated/JavaToLongBiFunction.swift b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaToLongBiFunction.swift similarity index 87% rename from Sources/JavaKitFunction/generated/JavaToLongBiFunction.swift rename to Sources/JavaStdlib/JavaUtilFunction/generated/JavaToLongBiFunction.swift index 93d0fc471..1d5fc7391 100644 --- a/Sources/JavaKitFunction/generated/JavaToLongBiFunction.swift +++ b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaToLongBiFunction.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.util.function.ToLongBiFunction") public struct JavaToLongBiFunction { diff --git a/Sources/JavaKitFunction/generated/JavaToLongFunction.swift b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaToLongFunction.swift similarity index 85% rename from Sources/JavaKitFunction/generated/JavaToLongFunction.swift rename to Sources/JavaStdlib/JavaUtilFunction/generated/JavaToLongFunction.swift index f4fd57675..66805be70 100644 --- a/Sources/JavaKitFunction/generated/JavaToLongFunction.swift +++ b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaToLongFunction.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface("java.util.function.ToLongFunction") public struct JavaToLongFunction { diff --git a/Sources/JavaKitFunction/generated/JavaUnaryOperator.swift b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaUnaryOperator.swift similarity index 94% rename from Sources/JavaKitFunction/generated/JavaUnaryOperator.swift rename to Sources/JavaStdlib/JavaUtilFunction/generated/JavaUnaryOperator.swift index 232283ba1..11dc00ee1 100644 --- a/Sources/JavaKitFunction/generated/JavaUnaryOperator.swift +++ b/Sources/JavaStdlib/JavaUtilFunction/generated/JavaUnaryOperator.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaInterface( "java.util.function.UnaryOperator", extends: JavaFunction.self) diff --git a/Sources/JavaKitFunction/swift-java.config b/Sources/JavaStdlib/JavaUtilFunction/swift-java.config similarity index 100% rename from Sources/JavaKitFunction/swift-java.config rename to Sources/JavaStdlib/JavaUtilFunction/swift-java.config diff --git a/Sources/JavaKitJar/generated/Attributes.swift b/Sources/JavaStdlib/JavaUtilJar/generated/Attributes.swift similarity index 98% rename from Sources/JavaKitJar/generated/Attributes.swift rename to Sources/JavaStdlib/JavaUtilJar/generated/Attributes.swift index f173c98d9..4897ebe3f 100644 --- a/Sources/JavaKitJar/generated/Attributes.swift +++ b/Sources/JavaStdlib/JavaUtilJar/generated/Attributes.swift @@ -1,7 +1,7 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaKitCollection -import JavaRuntime +import SwiftJava +import JavaUtil +import CSwiftJavaJNI @JavaClass("java.util.jar.Attributes") open class Attributes: JavaObject { diff --git a/Sources/JavaKitJar/generated/JarEntry.swift b/Sources/JavaStdlib/JavaUtilJar/generated/JarEntry.swift similarity index 98% rename from Sources/JavaKitJar/generated/JarEntry.swift rename to Sources/JavaStdlib/JavaUtilJar/generated/JarEntry.swift index 3a90d6a8f..adfb6d172 100644 --- a/Sources/JavaKitJar/generated/JarEntry.swift +++ b/Sources/JavaStdlib/JavaUtilJar/generated/JarEntry.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaClass("java.util.jar.JarEntry") open class JarEntry: ZipEntry { diff --git a/Sources/JavaKitJar/generated/JarFile.swift b/Sources/JavaStdlib/JavaUtilJar/generated/JarFile.swift similarity index 98% rename from Sources/JavaKitJar/generated/JarFile.swift rename to Sources/JavaStdlib/JavaUtilJar/generated/JarFile.swift index d5ea99697..e630afd2e 100644 --- a/Sources/JavaKitJar/generated/JarFile.swift +++ b/Sources/JavaStdlib/JavaUtilJar/generated/JarFile.swift @@ -1,7 +1,7 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaKitCollection -import JavaRuntime +import SwiftJava +import JavaUtil +import CSwiftJavaJNI @JavaClass("java.util.jar.JarFile") open class JarFile: JavaObject { diff --git a/Sources/JavaKitJar/generated/JarInputStream.swift b/Sources/JavaStdlib/JavaUtilJar/generated/JarInputStream.swift similarity index 98% rename from Sources/JavaKitJar/generated/JarInputStream.swift rename to Sources/JavaStdlib/JavaUtilJar/generated/JarInputStream.swift index f6d121d90..60f007fe9 100644 --- a/Sources/JavaKitJar/generated/JarInputStream.swift +++ b/Sources/JavaStdlib/JavaUtilJar/generated/JarInputStream.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaClass("java.util.jar.JarInputStream") open class JarInputStream: JavaObject { diff --git a/Sources/JavaKitJar/generated/JarOutputStream.swift b/Sources/JavaStdlib/JavaUtilJar/generated/JarOutputStream.swift similarity index 98% rename from Sources/JavaKitJar/generated/JarOutputStream.swift rename to Sources/JavaStdlib/JavaUtilJar/generated/JarOutputStream.swift index e13dc9734..bd27471f6 100644 --- a/Sources/JavaKitJar/generated/JarOutputStream.swift +++ b/Sources/JavaStdlib/JavaUtilJar/generated/JarOutputStream.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaClass("java.util.jar.JarOutputStream") open class JarOutputStream: JavaObject { diff --git a/Sources/JavaKitJar/generated/Manifest.swift b/Sources/JavaStdlib/JavaUtilJar/generated/Manifest.swift similarity index 94% rename from Sources/JavaKitJar/generated/Manifest.swift rename to Sources/JavaStdlib/JavaUtilJar/generated/Manifest.swift index 101af57c6..2033f41af 100644 --- a/Sources/JavaKitJar/generated/Manifest.swift +++ b/Sources/JavaStdlib/JavaUtilJar/generated/Manifest.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaClass("java.util.jar.Manifest") open class Manifest: JavaObject { diff --git a/Sources/JavaKitJar/generated/ZipEntry.swift b/Sources/JavaStdlib/JavaUtilJar/generated/ZipEntry.swift similarity index 99% rename from Sources/JavaKitJar/generated/ZipEntry.swift rename to Sources/JavaStdlib/JavaUtilJar/generated/ZipEntry.swift index 6a0fbd201..3066d54c7 100644 --- a/Sources/JavaKitJar/generated/ZipEntry.swift +++ b/Sources/JavaStdlib/JavaUtilJar/generated/ZipEntry.swift @@ -1,6 +1,6 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import SwiftJava +import CSwiftJavaJNI @JavaClass("java.util.zip.ZipEntry") open class ZipEntry: JavaObject { diff --git a/Sources/JavaKitJar/swift-java.config b/Sources/JavaStdlib/JavaUtilJar/swift-java.config similarity index 100% rename from Sources/JavaKitJar/swift-java.config rename to Sources/JavaStdlib/JavaUtilJar/swift-java.config diff --git a/Sources/JavaStdlib/README_PACKAGE_CONVENTION.md b/Sources/JavaStdlib/README_PACKAGE_CONVENTION.md new file mode 100644 index 000000000..ab65fa173 --- /dev/null +++ b/Sources/JavaStdlib/README_PACKAGE_CONVENTION.md @@ -0,0 +1,13 @@ +# Extracted Java Modules + +This directory contains Swift bindings for common Java standard library packages. +These pre-built bindings to solve a circular dependency problem - SwiftJava tools need these types to process and generate other bindings. + +You can also use these bindings directly in your SwiftJava programs to call Java classes without having to generate wrappers each time. + +The naming follows this pattern: Java package names become Swift target names. Example: `java.lang.util` becomes `JavaLangUtil`. + +Since Swift doesn't have namespaces like Java, all types appear at the top level in Swift. To avoid naming conflicts, +some types may be prefixed with 'J' (e.g. `JList` to avoid confusion with Swift native types). + +To see which Java types are included and any naming changes, check the `swift-java.config` file in each module. \ No newline at end of file diff --git a/Sources/JavaTypes/JavaAnnotation.swift b/Sources/JavaTypes/JavaAnnotation.swift new file mode 100644 index 000000000..a643c2987 --- /dev/null +++ b/Sources/JavaTypes/JavaAnnotation.swift @@ -0,0 +1,47 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +/// Describes a Java annotation (e.g. `@Deprecated` or `@Unsigned`) +public struct JavaAnnotation: Equatable, Hashable { + public let type: JavaType + public let arguments: [String] + + public init(className name: some StringProtocol, arguments: [String] = []) { + type = JavaType(className: name) + self.arguments = arguments + } + + public func render() -> String { + guard let className = type.className else { + fatalError("Java annotation must have a className") + } + + var res = "@\(className)" + guard !arguments.isEmpty else { + return res + } + + res += "(" + res += arguments.joined(separator: ",") + res += ")" + return res + } + +} + +extension JavaAnnotation { + public static var unsigned: JavaAnnotation { + JavaAnnotation(className: "Unsigned") + } +} \ No newline at end of file diff --git a/Sources/JavaTypes/JavaType+JNI.swift b/Sources/JavaTypes/JavaType+JNI.swift index 41a93d255..b9b00ccb8 100644 --- a/Sources/JavaTypes/JavaType+JNI.swift +++ b/Sources/JavaTypes/JavaType+JNI.swift @@ -14,7 +14,7 @@ extension JavaType { /// Map this Java type to the appropriate JNI type name. - var jniTypeName: String { + package var jniTypeName: String { switch self { case .boolean: "jboolean" case .float: "jfloat" @@ -34,9 +34,9 @@ extension JavaType { case .array(.float): "jfloatArray?" case .array(.double): "jdoubleArray?" case .array: "jobjectArray?" - case .class(package: "java.lang", name: "String"): "jstring?" - case .class(package: "java.lang", name: "Class"): "jclass?" - case .class(package: "java.lang", name: "Throwable"): "jthrowable?" + case .class(package: "java.lang", name: "String", _): "jstring?" + case .class(package: "java.lang", name: "Class", _): "jclass?" + case .class(package: "java.lang", name: "Throwable", _): "jthrowable?" case .class: "jobject?" } } diff --git a/Sources/JavaTypes/JavaType+JavaSource.swift b/Sources/JavaTypes/JavaType+JavaSource.swift index ccb4e96b5..f81f0c6d8 100644 --- a/Sources/JavaTypes/JavaType+JavaSource.swift +++ b/Sources/JavaTypes/JavaType+JavaSource.swift @@ -51,20 +51,37 @@ extension JavaType: CustomStringConvertible { case .double: "double" case .void: "void" case .array(let elementType): "\(elementType.description)[]" - case .class(package: let package, name: let name): + case .class(package: let package, name: let name, let typeParameters): if let package { - "\(package).\(name)" + if !typeParameters.isEmpty { + "\(package).\(name)<\(typeParameters.map(\.description).joined(separator: ", "))>" + } else { + "\(package).\(name)" + } } else { name } } } - /// Returns the a class name if this java type was a class, + /// Returns the class name if this java type was a class, /// and nil otherwise. public var className: String? { switch self { - case .class(_, let name): + case .class(_, let name, _): + return name + default: + return nil + } + } + + /// Returns the fully qualified class name if this java type was a class, + /// and nil otherwise. + public var fullyQualifiedClassName: String? { + switch self { + case .class(.some(let package), let name, _): + return "\(package).\(name)" + case .class(nil, let name, _): return name default: return nil diff --git a/Sources/JavaTypes/JavaType+SwiftNames.swift b/Sources/JavaTypes/JavaType+SwiftNames.swift index d01398b83..7015ef918 100644 --- a/Sources/JavaTypes/JavaType+SwiftNames.swift +++ b/Sources/JavaTypes/JavaType+SwiftNames.swift @@ -25,7 +25,7 @@ extension JavaType { .array: return false - case .class(package: "java.lang", name: "String"): + case .class(package: "java.lang", name: "String", _): return !stringIsValueType case .class: @@ -38,19 +38,26 @@ extension JavaType { case .boolean, .byte, .char, .short, .int, .long, .float, .double, .void, .array: return false - case .class(package: "java.lang", name: "Runnable"): + case .class(package: "java.lang", name: "Runnable", _): return true case .class: return false } } + public var isVoid: Bool { + if case .void = self { + return true + } + return false + } + public var isString: Bool { switch self { case .boolean, .byte, .char, .short, .int, .long, .float, .double, .void, .array: return false - case .class(package: "java.lang", name: "String"): + case .class(package: "java.lang", name: "String", _): return true case .class: return false @@ -78,20 +85,32 @@ extension JavaType { } } - /// Try to map a Swift type name (e.g., from the module Swift) over to a - /// primitive Java type, or fail otherwise. - public init?(swiftTypeName: String) { - switch swiftTypeName { - case "Bool": self = .boolean - case "Int8": self = .byte - case "UInt16": self = .char - case "Int16": self = .short - case "Int32": self = .int - case "Int64": self = .long - case "Float": self = .float - case "Double": self = .double - case "Void": self = .void - default: return nil - } - } +} + +/// Determines how type conversion should deal with Swift's unsigned numeric types. +/// +/// When `ignoreSign` is used, unsigned Swift types are imported directly as their corresponding bit-width types, +/// which may yield surprising values when an unsigned Swift value is interpreted as a signed Java type: +/// - `UInt8` is imported as `byte` +/// - `UInt16` is imported as `char` (this is always correct, since `char` is unsigned in Java) +/// - `UInt32` is imported as `int` +/// - `UInt64` is imported as `long` +/// +/// When `wrapUnsignedGuava` is used, unsigned Swift types are imported as safe "wrapper" types from the popular Guava +/// library on the Java side. SwiftJava does not include these types, so you would have to make sure your project depends +/// on Guava for such generated code to be able to compile. +/// +/// These make the Unsigned nature of the types explicit in Java, however they come at a cost of allocating the wrapper +/// object, and indirection when accessing the underlying numeric value. These are often useful as a signal to watch out +/// when dealing with a specific API, however in high performance use-cases, one may want to choose using the primitive +/// values directly, and interact with them using {@code UnsignedIntegers} SwiftKit helper classes on the Java side. +/// +/// The type mappings in this mode are as follows: +/// - `UInt8` is imported as `com.google.common.primitives.UnsignedInteger` +/// - `UInt16` is imported as `char` (this is always correct, since `char` is unsigned in Java) +/// - `UInt32` is imported as `com.google.common.primitives.UnsignedInteger` +/// - `UInt64` is imported as `com.google.common.primitives.UnsignedLong` +public enum UnsignedNumericsMode { + case ignoreSign + case wrapUnsignedGuava } diff --git a/Sources/JavaTypes/JavaType.swift b/Sources/JavaTypes/JavaType.swift index 6c5f5357c..ce7191bac 100644 --- a/Sources/JavaTypes/JavaType.swift +++ b/Sources/JavaTypes/JavaType.swift @@ -13,24 +13,32 @@ //===----------------------------------------------------------------------===// /// Describes the Java type system. +/// +/// Some types may need to be annotated when in parameter position, public enum JavaType: Equatable, Hashable { case boolean - case byte - case char - case short - case int - case long + case byte(parameterAnnotations: [JavaAnnotation]) + case char(parameterAnnotations: [JavaAnnotation]) + case short(parameterAnnotations: [JavaAnnotation]) + case int(parameterAnnotations: [JavaAnnotation]) + case long(parameterAnnotations: [JavaAnnotation]) case float case double case void /// A Java class separated into its package (e.g., "java.lang") and class name /// (e.g., "Object") - case `class`(package: String?, name: String) + case `class`(package: String?, name: String, typeParameters: [JavaType] = []) /// A Java array. indirect case array(JavaType) + public static var byte: JavaType { .byte(parameterAnnotations: []) } + public static var char: JavaType { .char(parameterAnnotations: []) } + public static var short: JavaType { .short(parameterAnnotations: []) } + public static var int: JavaType { .int(parameterAnnotations: []) } + public static var long: JavaType { .long(parameterAnnotations: []) } + /// Given a class name such as "java.lang.Object", split it into /// its package and class name to form a class instance. public init(className name: some StringProtocol) { @@ -45,6 +53,21 @@ public enum JavaType: Equatable, Hashable { } } +extension JavaType { + /// List of Java annotations this type should have include in parameter position, + /// e.g. `void example(@Unsigned long num)` + public var parameterAnnotations: [JavaAnnotation] { + switch self { + case .byte(let parameterAnnotations): return parameterAnnotations + case .char(let parameterAnnotations): return parameterAnnotations + case .short(let parameterAnnotations): return parameterAnnotations + case .int(let parameterAnnotations): return parameterAnnotations + case .long(let parameterAnnotations): return parameterAnnotations + default: return [] + } + } +} + extension JavaType { /// Whether this is a primitive Java type. public var isPrimitive: Bool { @@ -57,3 +80,4 @@ extension JavaType { } } } + diff --git a/Sources/JavaTypes/Mangling.swift b/Sources/JavaTypes/Mangling.swift index 74e2e0b83..1311b7179 100644 --- a/Sources/JavaTypes/Mangling.swift +++ b/Sources/JavaTypes/Mangling.swift @@ -35,8 +35,8 @@ extension JavaType { case .short: "S" case .void: "V" case .array(let elementType): "[" + elementType.mangledName - case .class(package: let package, name: let name): - "L\(package!).\(name);".replacingPeriodsWithSlashes() + case .class(package: let package, name: let name, _): + "L\(package!).\(name.replacingPeriodsWithDollars());".replacingPeriodsWithSlashes() } } } @@ -145,4 +145,9 @@ extension StringProtocol { fileprivate func replacingSlashesWithPeriods() -> String { return String(self.map { $0 == "/" ? "." as Character : $0 }) } + + /// Return the string after replacing all of the periods (".") with slashes ("$"). + fileprivate func replacingPeriodsWithDollars() -> String { + return String(self.map { $0 == "." ? "$" as Character : $0 }) + } } diff --git a/Sources/JavaKit/AnyJavaObject.swift b/Sources/SwiftJava/AnyJavaObject.swift similarity index 90% rename from Sources/JavaKit/AnyJavaObject.swift rename to Sources/SwiftJava/AnyJavaObject.swift index 5e0a88d03..33a83159c 100644 --- a/Sources/JavaKit/AnyJavaObject.swift +++ b/Sources/SwiftJava/AnyJavaObject.swift @@ -12,7 +12,7 @@ // //===----------------------------------------------------------------------===// -import JavaRuntime +import CSwiftJavaJNI /// Protocol that describes Swift types that are bridged to a Java class type. /// @@ -52,17 +52,21 @@ public protocol AnyJavaObject { /// Protocol that allows Swift types to specify a custom Java class loader on /// initialization. This is useful for platforms (e.g. Android) where the default /// class loader does not make all application classes visible. -public protocol CustomJavaClassLoader: AnyJavaObject { +public protocol AnyJavaObjectWithCustomClassLoader: AnyJavaObject { static func getJavaClassLoader(in environment: JNIEnvironment) throws -> JavaClassLoader! } extension AnyJavaObject { /// Retrieve the underlying Java object. public var javaThis: jobject { - javaHolder.object! + javaHolder.object! // FIXME: this is a bad idea, can be null + } + + public var javaThisOptional: jobject? { + javaHolder.object } - /// Retrieve the environment in which this Java object resides. + /// Retrieve the environment in which this Java object was created. public var javaEnvironment: JNIEnvironment { javaHolder.environment } @@ -118,8 +122,8 @@ extension AnyJavaObject { in environment: JNIEnvironment, _ body: (jclass) throws -> Result ) throws -> Result { - if let customJavaClassLoader = self as? CustomJavaClassLoader.Type, - let customClassLoader = try customJavaClassLoader.getJavaClassLoader(in: environment) { + if let AnyJavaObjectWithCustomClassLoader = self as? AnyJavaObjectWithCustomClassLoader.Type, + let customClassLoader = try AnyJavaObjectWithCustomClassLoader.getJavaClassLoader(in: environment) { try _withJNIClassFromCustomClassLoader(customClassLoader, in: environment, body) } else { try _withJNIClassFromDefaultClassLoader(in: environment, body) diff --git a/Sources/JavaKit/BridgedValues/JavaValue+Array.swift b/Sources/SwiftJava/BridgedValues/JavaValue+Array.swift similarity index 100% rename from Sources/JavaKit/BridgedValues/JavaValue+Array.swift rename to Sources/SwiftJava/BridgedValues/JavaValue+Array.swift diff --git a/Sources/JavaKit/BridgedValues/JavaValue+Bool.swift b/Sources/SwiftJava/BridgedValues/JavaValue+Bool.swift similarity index 100% rename from Sources/JavaKit/BridgedValues/JavaValue+Bool.swift rename to Sources/SwiftJava/BridgedValues/JavaValue+Bool.swift diff --git a/Sources/JavaKit/BridgedValues/JavaValue+FloatingPoint.swift b/Sources/SwiftJava/BridgedValues/JavaValue+FloatingPoint.swift similarity index 100% rename from Sources/JavaKit/BridgedValues/JavaValue+FloatingPoint.swift rename to Sources/SwiftJava/BridgedValues/JavaValue+FloatingPoint.swift diff --git a/Sources/SwiftJava/BridgedValues/JavaValue+Integers.swift b/Sources/SwiftJava/BridgedValues/JavaValue+Integers.swift new file mode 100644 index 000000000..005753eb6 --- /dev/null +++ b/Sources/SwiftJava/BridgedValues/JavaValue+Integers.swift @@ -0,0 +1,584 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JavaTypes + +extension UInt8: JavaValue { + public typealias JNIType = jbyte + + public static var jvalueKeyPath: WritableKeyPath { \.b } + + public static var javaType: JavaType { .byte } + + /// Retrieve the JNI value. + public func getJNIValue(in environment: JNIEnvironment) -> JNIType { JNIType(self) } + + /// Initialize from a JNI value. + public init(fromJNI value: JNIType, in environment: JNIEnvironment) { + self = Self(value) + } + + public static func jniMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallByteMethodA + } + + public static func jniFieldGet(in environment: JNIEnvironment) -> JNIFieldGet { + environment.interface.GetByteField + } + + public static func jniFieldSet(in environment: JNIEnvironment) -> JNIFieldSet { + environment.interface.SetByteField + } + + public static func jniStaticMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallStaticByteMethodA + } + + public static func jniStaticFieldGet(in environment: JNIEnvironment) -> JNIStaticFieldGet { + environment.interface.GetStaticByteField + } + + public static func jniStaticFieldSet(in environment: JNIEnvironment) -> JNIStaticFieldSet { + environment.interface.SetStaticByteField + } + + public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray { + environment.interface.NewByteArray + } + + public static func jniGetArrayRegion(in environment: JNIEnvironment) -> JNIGetArrayRegion { + environment.interface.GetByteArrayRegion + } + + public static func jniSetArrayRegion(in environment: JNIEnvironment) -> JNISetArrayRegion { + environment.interface.SetByteArrayRegion + } + + public static var jniPlaceholderValue: jbyte { + 0 + } +} + +extension Int8: JavaValue { + public typealias JNIType = jbyte + + public static var jvalueKeyPath: WritableKeyPath { \.b } + + public static var javaType: JavaType { .byte } + + public static func jniMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallByteMethodA + } + + public static func jniFieldGet(in environment: JNIEnvironment) -> JNIFieldGet { + environment.interface.GetByteField + } + + public static func jniFieldSet(in environment: JNIEnvironment) -> JNIFieldSet { + environment.interface.SetByteField + } + + public static func jniStaticMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallStaticByteMethodA + } + + public static func jniStaticFieldGet(in environment: JNIEnvironment) -> JNIStaticFieldGet { + environment.interface.GetStaticByteField + } + + public static func jniStaticFieldSet(in environment: JNIEnvironment) -> JNIStaticFieldSet { + environment.interface.SetStaticByteField + } + + public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray { + environment.interface.NewByteArray + } + + public static func jniGetArrayRegion(in environment: JNIEnvironment) -> JNIGetArrayRegion { + environment.interface.GetByteArrayRegion + } + + public static func jniSetArrayRegion(in environment: JNIEnvironment) -> JNISetArrayRegion { + environment.interface.SetByteArrayRegion + } + + public static var jniPlaceholderValue: jbyte { + 0 + } +} + +extension UInt16: JavaValue { + public typealias JNIType = jchar + + public static var jvalueKeyPath: WritableKeyPath { \.c } + + public static var javaType: JavaType { .char } + + public static func jniMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallCharMethodA + } + + public static func jniFieldGet(in environment: JNIEnvironment) -> JNIFieldGet { + environment.interface.GetCharField + } + + public static func jniFieldSet(in environment: JNIEnvironment) -> JNIFieldSet { + environment.interface.SetCharField + } + + public static func jniStaticMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallStaticCharMethodA + } + + public static func jniStaticFieldGet(in environment: JNIEnvironment) -> JNIStaticFieldGet { + environment.interface.GetStaticCharField + } + + public static func jniStaticFieldSet(in environment: JNIEnvironment) -> JNIStaticFieldSet { + environment.interface.SetStaticCharField + } + + public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray { + environment.interface.NewCharArray + } + + public static func jniGetArrayRegion(in environment: JNIEnvironment) -> JNIGetArrayRegion { + environment.interface.GetCharArrayRegion + } + + public static func jniSetArrayRegion(in environment: JNIEnvironment) -> JNISetArrayRegion { + environment.interface.SetCharArrayRegion + } + + public static var jniPlaceholderValue: jchar { + 0 + } +} + +extension Int16: JavaValue { + public typealias JNIType = jshort + + public static var jvalueKeyPath: WritableKeyPath { \.s } + + public static var javaType: JavaType { .short } + + public static func jniMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallShortMethodA + } + + public static func jniFieldGet(in environment: JNIEnvironment) -> JNIFieldGet { + environment.interface.GetShortField + } + + public static func jniFieldSet(in environment: JNIEnvironment) -> JNIFieldSet { + environment.interface.SetShortField + } + + public static func jniStaticMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallStaticShortMethodA + } + + public static func jniStaticFieldGet(in environment: JNIEnvironment) -> JNIStaticFieldGet { + environment.interface.GetStaticShortField + } + + public static func jniStaticFieldSet(in environment: JNIEnvironment) -> JNIStaticFieldSet { + environment.interface.SetStaticShortField + } + + public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray { + environment.interface.NewShortArray + } + + public static func jniGetArrayRegion(in environment: JNIEnvironment) -> JNIGetArrayRegion { + environment.interface.GetShortArrayRegion + } + + public static func jniSetArrayRegion(in environment: JNIEnvironment) -> JNISetArrayRegion { + environment.interface.SetShortArrayRegion + } + + public static var jniPlaceholderValue: jshort { + 0 + } +} + +extension UInt32: JavaValue { + public typealias JNIType = jint + + public static var jvalueKeyPath: WritableKeyPath { \.i } + + public static var javaType: JavaType { .int } + + /// Retrieve the JNI value. + public func getJNIValue(in environment: JNIEnvironment) -> JNIType { JNIType(self) } + + /// Initialize from a JNI value. + public init(fromJNI value: JNIType, in environment: JNIEnvironment) { + self = Self(value) + } + + public static func jniMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallIntMethodA + } + + public static func jniFieldGet(in environment: JNIEnvironment) -> JNIFieldGet { + environment.interface.GetIntField + } + + public static func jniFieldSet(in environment: JNIEnvironment) -> JNIFieldSet { + environment.interface.SetIntField + } + + public static func jniStaticMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallStaticIntMethodA + } + + public static func jniStaticFieldGet(in environment: JNIEnvironment) -> JNIStaticFieldGet { + environment.interface.GetStaticIntField + } + + public static func jniStaticFieldSet(in environment: JNIEnvironment) -> JNIStaticFieldSet { + environment.interface.SetStaticIntField + } + + public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray { + environment.interface.NewIntArray + } + + public static func jniGetArrayRegion(in environment: JNIEnvironment) -> JNIGetArrayRegion { + environment.interface.GetIntArrayRegion + } + + public static func jniSetArrayRegion(in environment: JNIEnvironment) -> JNISetArrayRegion { + environment.interface.SetIntArrayRegion + } + + public static var jniPlaceholderValue: jint { + 0 + } +} + +extension Int32: JavaValue { + public typealias JNIType = jint + + public static var jvalueKeyPath: WritableKeyPath { \.i } + + public func getJNIValue(in environment: JNIEnvironment) -> JNIType { JNIType(self) } + + public init(fromJNI value: JNIType, in environment: JNIEnvironment) { + self = Int32(value) + } + + public static var javaType: JavaType { .int } + + public static func jniMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallIntMethodA + } + + public static func jniFieldGet(in environment: JNIEnvironment) -> JNIFieldGet { + environment.interface.GetIntField + } + + public static func jniFieldSet(in environment: JNIEnvironment) -> JNIFieldSet { + environment.interface.SetIntField + } + + public static func jniStaticMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallStaticIntMethodA + } + + public static func jniStaticFieldGet(in environment: JNIEnvironment) -> JNIStaticFieldGet { + environment.interface.GetStaticIntField + } + + public static func jniStaticFieldSet(in environment: JNIEnvironment) -> JNIStaticFieldSet { + environment.interface.SetStaticIntField + } + + public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray { + environment.interface.NewIntArray + } + + public static func jniGetArrayRegion(in environment: JNIEnvironment) -> JNIGetArrayRegion { + environment.interface.GetIntArrayRegion + } + + public static func jniSetArrayRegion(in environment: JNIEnvironment) -> JNISetArrayRegion { + environment.interface.SetIntArrayRegion + } + + public static var jniPlaceholderValue: jint { + 0 + } +} + +extension UInt64: JavaValue { + public typealias JNIType = jlong + + public static var jvalueKeyPath: WritableKeyPath { \.j } + + public func getJNIValue(in environment: JNIEnvironment) -> JNIType { JNIType(self) } + + public init(fromJNI value: JNIType, in environment: JNIEnvironment) { + self = UInt64(value) + } + + public static var javaType: JavaType { .long } + + public static func jniMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallLongMethodA + } + + public static func jniFieldGet(in environment: JNIEnvironment) -> JNIFieldGet { + environment.interface.GetLongField + } + + public static func jniFieldSet(in environment: JNIEnvironment) -> JNIFieldSet { + environment.interface.SetLongField + } + + public static func jniStaticMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallStaticLongMethodA + } + + public static func jniStaticFieldGet(in environment: JNIEnvironment) -> JNIStaticFieldGet { + environment.interface.GetStaticLongField + } + + public static func jniStaticFieldSet(in environment: JNIEnvironment) -> JNIStaticFieldSet { + environment.interface.SetStaticLongField + } + + public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray { + environment.interface.NewLongArray + } + + public static func jniGetArrayRegion(in environment: JNIEnvironment) -> JNIGetArrayRegion { + environment.interface.GetLongArrayRegion + } + + public static func jniSetArrayRegion(in environment: JNIEnvironment) -> JNISetArrayRegion { + environment.interface.SetLongArrayRegion + } + + public static var jniPlaceholderValue: jlong { + 0 + } +} + +extension Int64: JavaValue { + public typealias JNIType = jlong + + public static var jvalueKeyPath: WritableKeyPath { \.j } + + public func getJNIValue(in environment: JNIEnvironment) -> JNIType { JNIType(self) } + + public init(fromJNI value: JNIType, in environment: JNIEnvironment) { + self = Int64(value) + } + + public static var javaType: JavaType { .long } + + public static func jniMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallLongMethodA + } + + public static func jniFieldGet(in environment: JNIEnvironment) -> JNIFieldGet { + environment.interface.GetLongField + } + + public static func jniFieldSet(in environment: JNIEnvironment) -> JNIFieldSet { + environment.interface.SetLongField + } + + public static func jniStaticMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallStaticLongMethodA + } + + public static func jniStaticFieldGet(in environment: JNIEnvironment) -> JNIStaticFieldGet { + environment.interface.GetStaticLongField + } + + public static func jniStaticFieldSet(in environment: JNIEnvironment) -> JNIStaticFieldSet { + environment.interface.SetStaticLongField + } + + public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray { + environment.interface.NewLongArray + } + + public static func jniGetArrayRegion(in environment: JNIEnvironment) -> JNIGetArrayRegion { + environment.interface.GetLongArrayRegion + } + + public static func jniSetArrayRegion(in environment: JNIEnvironment) -> JNISetArrayRegion { + environment.interface.SetLongArrayRegion + } + + public static var jniPlaceholderValue: jlong { + 0 + } +} + +#if _pointerBitWidth(_32) +extension Int: JavaValue { + + public typealias JNIType = jint + + public static var jvalueKeyPath: WritableKeyPath { \.i } + + public func getJNIValue(in environment: JNIEnvironment) -> JNIType { JNIType(self) } + + public init(fromJNI value: JNIType, in environment: JNIEnvironment) { + self = Int(value) + } + + public static var javaType: JavaType { .int } + + public static func jniMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallIntMethodA + } + + public static func jniFieldGet(in environment: JNIEnvironment) -> JNIFieldGet { + environment.interface.GetIntField + } + + public static func jniFieldSet(in environment: JNIEnvironment) -> JNIFieldSet { + environment.interface.SetIntField + } + + public static func jniStaticMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallStaticIntMethodA + } + + public static func jniStaticFieldGet(in environment: JNIEnvironment) -> JNIStaticFieldGet { + environment.interface.GetStaticIntField + } + + public static func jniStaticFieldSet(in environment: JNIEnvironment) -> JNIStaticFieldSet { + environment.interface.SetStaticIntField + } + + public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray { + environment.interface.NewIntArray + } + + public static func jniGetArrayRegion(in environment: JNIEnvironment) -> JNIGetArrayRegion { + environment.interface.GetIntArrayRegion + } + + public static func jniSetArrayRegion(in environment: JNIEnvironment) -> JNISetArrayRegion { + environment.interface.SetIntArrayRegion + } + + public static var jniPlaceholderValue: jint { + 0 + } +} +#elseif _pointerBitWidth(_64) +extension Int: JavaValue { + public typealias JNIType = jlong + + public static var jvalueKeyPath: WritableKeyPath { \.j } + + public func getJNIValue(in environment: JNIEnvironment) -> JNIType { JNIType(self) } + + public init(fromJNI value: JNIType, in environment: JNIEnvironment) { + self = Int(value) + } + + public static var javaType: JavaType { .long } + + public static func jniMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallLongMethodA + } + + public static func jniFieldGet(in environment: JNIEnvironment) -> JNIFieldGet { + environment.interface.GetLongField + } + + public static func jniFieldSet(in environment: JNIEnvironment) -> JNIFieldSet { + environment.interface.SetLongField + } + + public static func jniStaticMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallStaticLongMethodA + } + + public static func jniStaticFieldGet(in environment: JNIEnvironment) -> JNIStaticFieldGet { + environment.interface.GetStaticLongField + } + + public static func jniStaticFieldSet(in environment: JNIEnvironment) -> JNIStaticFieldSet { + environment.interface.SetStaticLongField + } + + public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray { + environment.interface.NewLongArray + } + + public static func jniGetArrayRegion(in environment: JNIEnvironment) -> JNIGetArrayRegion { + environment.interface.GetLongArrayRegion + } + + public static func jniSetArrayRegion(in environment: JNIEnvironment) -> JNISetArrayRegion { + environment.interface.SetLongArrayRegion + } + + public static var jniPlaceholderValue: jlong { + 0 + } +} +#endif diff --git a/Sources/JavaKit/BridgedValues/JavaValue+String.swift b/Sources/SwiftJava/BridgedValues/JavaValue+String.swift similarity index 100% rename from Sources/JavaKit/BridgedValues/JavaValue+String.swift rename to Sources/SwiftJava/BridgedValues/JavaValue+String.swift diff --git a/Sources/JavaKit/JavaRuntime+Reexport.swift b/Sources/SwiftJava/CSwiftJavaJNI+Reexport.swift similarity index 93% rename from Sources/JavaKit/JavaRuntime+Reexport.swift rename to Sources/SwiftJava/CSwiftJavaJNI+Reexport.swift index e65d4e182..9b2c02a82 100644 --- a/Sources/JavaKit/JavaRuntime+Reexport.swift +++ b/Sources/SwiftJava/CSwiftJavaJNI+Reexport.swift @@ -12,4 +12,4 @@ // //===----------------------------------------------------------------------===// -@_exported import JavaRuntime +@_exported import CSwiftJavaJNI diff --git a/USER_GUIDE.md b/Sources/SwiftJava/Documentation.docc/SwiftJava.md similarity index 55% rename from USER_GUIDE.md rename to Sources/SwiftJava/Documentation.docc/SwiftJava.md index 2abe8ea0b..d4b5dacd7 100644 --- a/USER_GUIDE.md +++ b/Sources/SwiftJava/Documentation.docc/SwiftJava.md @@ -1,15 +1,11 @@ -# JavaKit +# SwiftJava Library and tools to make it easy to use Java libraries from Swift using the Java Native Interface (JNI). -## Getting started +## SwiftJava: Using Java libraries from Swift -Before using this package, set the `JAVA_HOME` environment variable to point at your Java installation. Failing to do so will produce errors when processing the package manifest. Alternatively, you can put the path to your Java installation in the file `~/.java_home`. - -### Using Java libraries from Swift - -Existing Java libraries can be wrapped for use in Swift with the `Java2Swift` -tool. In a Swift program, the most direct way to access a Java API is to use the SwiftPM plugin to provide Swift wrappers for the Java classes. To do so, add a configuration file `Java2Swift.config` into the source directory for the Swift target. This is a JSON file that specifies Java classes and the Swift type name that should be generated to wrap them. For example, the following file maps `java.math.BigInteger` to a Swift type named `BigInteger`: +Existing Java libraries can be wrapped for use in Swift with the `swift-java` +tool. In a Swift program, the most direct way to access a Java API is to use the SwiftPM plugin to provide Swift wrappers for the Java classes. To do so, add a configuration file `swift-java.config` into the source directory for the Swift target. This is a JSON file that specifies Java classes and the Swift type name that should be generated to wrap them. For example, the following file maps `java.math.BigInteger` to a Swift type named `BigInteger`: ```json { @@ -31,11 +27,11 @@ or, equivalently, adding the following to the package dependencies: .package(url: "https://github.com/swiftlang/swift-java", branch: "main"), ``` -Finally, update `Package.swift` so that the `Java2SwiftPlugin` plugin runs on the target in which you want to generate Swift wrappers. The plugin looks like this: +Finally, update `Package.swift` so that the `SwiftJavaPlugin` plugin runs on the target in which you want to generate Swift wrappers. The plugin looks like this: ```swift plugins: [ - .plugin(name: "Java2SwiftPlugin", package: "swift-java"), + .plugin(name: "SwiftJavaPlugin", package: "swift-java"), ] ``` @@ -81,7 +77,7 @@ Swift ensures that the Java garbage collector will keep the object alive until ` ### Creating a Java Virtual Machine instance from Swift -When JavaKit requires a running Java Virtual Machine to use an operation (for example, to create an instance of `BigInteger`), it will query to determine if one is running and, if not, create one. To exercise more control over the creation and configuration of the Java virtual machine, use the `JavaVirtualMachine` class, which provides creation and query operations. One can create a shared instance by calling `JavaVirtualMachine.shared()`, optionally passing along extra options to the JVM (such as the class path): +When SwiftJava requires a running Java Virtual Machine to use an operation (for example, to create an instance of `BigInteger`), it will query to determine if one is running and, if not, create one. To exercise more control over the creation and configuration of the Java virtual machine, use the `JavaVirtualMachine` class, which provides creation and query operations. One can create a shared instance by calling `JavaVirtualMachine.shared()`, optionally passing along extra options to the JVM (such as the class path): ```swift let javaVirtualMachine = try JavaVirtualMachine.shared() @@ -101,10 +97,10 @@ let bigInt = BigInteger(veryBigNumber, environment: jniEnvironment) ### Importing a Jar file into Swift -Java libraries are often distributed as Jar files. The `Java2Swift` tool can inspect a Jar file to create a `Java2Swift.config` file that will wrap all of the public classes for use in Swift. Following the example in `swift-java/Samples/JavaSieve`, we will wrap a small [Java library for computing prime numbers](https://github.com/gazman-sdk/quadratic-sieve-Java) for use in Swift. Assuming we have a Jar file `QuadraticSieve-1.0.jar` in the package directory, run the following command: +Java libraries are often distributed as Jar files. The `swift-java` tool can inspect a Jar file to create a `swift-java.config` file that will wrap all of the public classes for use in Swift. Following the example in `swift-java/Samples/JavaSieve`, we will wrap a small [Java library for computing prime numbers](https://github.com/gazman-sdk/quadratic-sieve-Java) for use in Swift. Assuming we have a Jar file `QuadraticSieve-1.0.jar` in the package directory, run the following command: ```swift -swift run Java2Swift --module-name JavaSieve --jar QuadraticSieve-1.0.jar +swift-java configure --swift-module JavaSieve --jar QuadraticSieve-1.0.jar ``` The resulting configuration file will look something like this: @@ -142,7 +138,7 @@ The resulting configuration file will look something like this: } ``` -As with the previous `JavaProbablyPrime` sample, the `JavaSieve` target in `Package.swift` should depend on the `swift-java` package modules (`JavaKit`) and apply the `Java2Swift` plugin. This makes all of the Java classes found in the Jar file available to Swift within the `JavaSieve` target. +As with the previous `JavaProbablyPrime` sample, the `JavaSieve` target in `Package.swift` should depend on the `swift-java` package modules (`SwiftJava`) and apply the `swift-java` plugin. This makes all of the Java classes found in the Jar file available to Swift within the `JavaSieve` target. If you inspect the build output, there are a number of warnings that look like this: @@ -156,15 +152,15 @@ These warnings mean that some of the APIs in the Java library aren't available i .target( name: "JavaMath", dependencies: [ - .product(name: "JavaKit", package: "swift-java"), + .product(name: "SwiftJava", package: "swift-java"), ], plugins: [ - .plugin(name: "Java2SwiftPlugin", package: "swift-java"), + .plugin(name: "SwiftJavaPlugin", package: "swift-java"), ] ), ``` -Then define a a Java2Swift configuration file in `Sources/JavaMath/Java2Swift.config` to bring in the types we need: +Then define a a swift-java configuration file in `Sources/JavaMath/swift-java.config` to bring in the types we need: ```json { @@ -204,7 +200,7 @@ let primes = sieveClass.findPrimes(100) // returns a List? Putting it all together, we can define a main program in `Sources/JavaSieve/main.swift` that looks like this: ```swift -import JavaKit +import SwiftJNI let jvm = try JavaVirtualMachine.shared(classpath: ["QuadraticSieve-1.0.jar"]) do { @@ -221,7 +217,7 @@ Note that we are passing the Jar file in the `classpath` argument when initializ ### Downcasting -All Java classes available in Swift provide `is` and `as` methods to check whether an object dynamically matches another type. The `is` operation is the equivalent of Java's `instanceof` and Swift's `is` operator, and will checkin whether a given object is of the specified type, e.g., +All Java classes available in Swift provide `is` and `as` methods to check whether an object dynamically matches another type. The `is` operation is the equivalent of Java's `instanceof` and Swift's `is` operator, and will checking whether a given object is of the specified type, e.g., ```swift if myObject.is(URL.self) { @@ -241,10 +237,10 @@ if let url = myObject.as(URL.self) { ### Implementing Java `native` methods in Swift -JavaKit supports implementing Java `native` methods in Swift using JNI with the `@JavaImplementation` macro. In Java, the method must be declared as `native`, e.g., +SwiftJava supports implementing Java `native` methods in Swift using JNI with the `@JavaImplementation` macro. In Java, the method must be declared as `native`, e.g., ```java -package org.swift.javakit.example; +package org.swift.swiftjava.example; public class HelloSwift { static { @@ -255,20 +251,20 @@ public class HelloSwift { } ``` -On the Swift side, the Java class needs to be exposed to Swift through `Java2Swift.config`, e.g.,: +On the Swift side, the Java class needs to be exposed to Swift through `swift-java.config`, e.g.,: ```swift { "classes" : { - "org.swift.javakit.example.HelloSwift" : "Hello", + "org.swift.swiftjava.example.HelloSwift" : "Hello", } } ``` -Implementations of `native` methods are written in an extension of the Swift type that has been marked with `@JavaImplementation`. The methods themselves must be marked with `@JavaMethod`, indicating that they are available to Java as well. To help ensure that the Swift code implements all of the `native` methods with the right signatures, JavaKit produces a protocol with the Swift type name suffixed by `NativeMethods`. Declare conformance to that protocol and implement its requirements, for example: +Implementations of `native` methods are written in an extension of the Swift type that has been marked with `@JavaImplementation`. The methods themselves must be marked with `@JavaMethod`, indicating that they are available to Java as well. To help ensure that the Swift code implements all of the `native` methods with the right signatures, SwiftJava produces a protocol with the Swift type name suffixed by `NativeMethods`. Declare conformance to that protocol and implement its requirements, for example: ```swift -@JavaImplementation("org.swift.javakit.HelloSwift") +@JavaImplementation("org.swift.swiftjava.HelloSwift") extension Hello: HelloNativeMethods { @JavaMethod func reportStatistics(_ meaning: String, _ numbers: [Double]) -> String { @@ -282,7 +278,7 @@ Java native methods that throw any checked exception should be marked as `throws The Swift implementations of Java `native` constructors and static methods require an additional Swift parameter `environment: JNIEnvironment? = nil`, which will receive the JNI environment in which the function is being executed. In case of nil, the `JavaVirtualMachine.shared().environment()` value will be used. -## Using Java libraries from Swift +## SwiftJava: Using Java libraries from Swift This section describes how Java libraries and mapped into Swift and their use from Swift. @@ -357,7 +353,7 @@ for entry in jarFile.entries()! { `JavaMethod` is a [function body macro](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0415-function-body-macros.md) that translates the argument and result types to/from Java and performs a call to the named method via JNI. -A Java method or constructor that throws a checked exception should be marked as `throws` in Swift. Swift's projection of Java throwable types (as `JavaKit.Throwable`) conforms to the Swift `Error` protocol, so Java exceptions will be rethrown as Swift errors. +A Java method or constructor that throws a checked exception should be marked as `throws` in Swift. Swift's projection of Java throwable types (as `SwiftJava.Throwable`) conforms to the Swift `Error` protocol, so Java exceptions will be rethrown as Swift errors. ### Java <-> Swift Type mapping @@ -384,16 +380,16 @@ For Swift projections of Java classes, the Swift type itself conforms to the `An Because Java has implicitly nullability of references, `AnyJavaObject` types do not directly conform to `JavaValue`: rather, optionals of `AnyJavaObject`-conforming type conform to `JavaValue`. This requires Swift code to deal with the optionality at interface boundaries rather than invite implicit NULL pointer dereferences. -A number of JavaKit modules provide Swift projections of Java classes and interfaces. Here are a few: +A number of SwiftJava modules provide Swift projections of Java classes and interfaces. Here are a few: | Java class | Swift class | Swift module | | --------------------- | -------------- | ---------------- | -| `java.lang.Object` | `JavaObject` | `JavaKit` | -| `java.lang.Class` | `JavaClass` | `JavaKit` | -| `java.lang.Throwable` | `Throwable` | `JavaKit` | -| `java.net.URL` | `URL` | `JavaKitNetwork` | +| `java.lang.Object` | `JavaObject` | `SwiftJava` | +| `java.lang.Class` | `JavaClass` | `SwiftJava` | +| `java.lang.Throwable` | `Throwable` | `SwiftJava` | +| `java.net.URL` | `URL` | `JavaNet` | -The `Java2Swift` tool can translate any other Java classes into Swift projections. The easiest way to use `Java2Swift` is with the SwiftPM plugin described above. More information about using this tool directly are provided later in this document +The `swift-java` tool can translate any other Java classes into Swift projections. The easiest way to use `swift-java` is with the SwiftPM plugin described above. More information about using this tool directly are provided later in this document #### Improve parameter names of imported Java methods When building Java libraries you can pass the `-parameters` option to javac @@ -410,7 +406,7 @@ When building Java sources using the JavaCompilerPlugin this option is passed by ### Class objects and static methods -Every `AnyJavaObject` has a property `javaClass` that provides an instance of `JavaClass` specialized to the type. For example, `url.javaClass` will produce an instance of `JavaClass`. The `JavaClass` instance is a wrapper around a Java class object (`java.lang.Class`) that has two roles in Swift. First, it provides access to all of the APIs on the Java class object. The `JavaKitReflection` library, for example, exposes these APIs and the types they depend on (`Method`, +Every `AnyJavaObject` has a property `javaClass` that provides an instance of `JavaClass` specialized to the type. For example, `url.javaClass` will produce an instance of `JavaClass`. The `JavaClass` instance is a wrapper around a Java class object (`java.lang.Class`) that has two roles in Swift. First, it provides access to all of the APIs on the Java class object. The `JavaLangReflect` library, for example, exposes these APIs and the types they depend on (`Method`, `Constructor`, etc.) for dynamic reflection. Second, the `JavaClass` provides access to the `static` methods on the Java class. For example, [`java.net.URLConnection`](https://docs.oracle.com/javase/8/docs/api/java/net/URLConnection.html) has static methods to access default settings, such as the default for the `allowUserInteraction` field. These are exposed as instance methods on `JavaClass`, e.g., ```swift @@ -420,7 +416,7 @@ extension JavaClass { } ``` -### Interfaces +### Java Interfaces Java interfaces are similar to classes, and are projected into Swift in much the same way, but with the macro `JavaInterface`. The `JavaInterface` macro takes the Java interface name as well as any Java interfaces that this interface extends. As an example, here is the Swift projection of the [`java.util.Enumeration`](https://docs.oracle.com/javase/8/docs/api/java/util/Enumeration.html) generic interface: @@ -437,240 +433,3 @@ public struct Enumeration { public func nextElement() -> JavaObject! } ``` - -## Translating Java classes with `Java2Swift` - -The `Java2Swift` is a Swift program that uses Java's runtime reflection facilities to translate the requested Java classes into their Swift projections. The output is a number of Swift source files, each of which corresponds to a -single Java class. The `Java2Swift` can be executed like this: - -``` -swift run Java2Swift -``` - -to produce help output like the following: - -``` -USAGE: Java2Swift --module-name [--depends-on ...] [--jar] [--cp ...] [--output-directory ] - -ARGUMENTS: - The input file, which is either a Java2Swift - configuration file or (if '-jar' was specified) - a Jar file. - -OPTIONS: - --module-name - The name of the Swift module into which the resulting - Swift types will be generated. - --depends-on - A Java2Swift configuration file for a given Swift - module name on which this module depends, e.g., - JavaKitJar=Sources/JavaKitJar/Java2Swift.config. - There should be one of these options for each Swift - module that this module depends on (transitively) - that contains wrapped Java sources. - --jar Specifies that the input is a Jar file whose public - classes will be loaded. The output of Java2Swift will - be a configuration file (Java2Swift.config) that can - be used as input to a subsequent Java2Swift - invocation to generate wrappers for those public - classes. - --cp, --classpath Class search path of directories and zip/jar files - from which Java classes can be loaded. - -o, --output-directory - The directory in which to output the generated Swift - files or the Java2Swift configuration file. (default: - .) - -h, --help Show help information. -``` - -For example, the `JavaKitJar` library is generated with this command line: - -```swift -swift run Java2Swift --module-name JavaKitJar --depends-on JavaKit=Sources/JavaKit/Java2Swift.config -o Sources/JavaKitJar/generated Sources/JavaKitJar/Java2Swift.config -``` - -The `--module-name JavaKitJar` parameter describes the name of the Swift module in which the code will be generated. - -The `--depends-on` option is followed by the Java2Swift configuration files for any library on which this Swift library depends. Each `--depends-on` option is of the form `=`, and tells Java2Swift which other Java classes have already been translated to Swift. For example, if your Java class uses `java.net.URL`, then you should include -`JavaKitNetwork`'s configuration file as a dependency here. - -The `-o` option specifies the output directory. Typically, this will be `Sources//generated` or similar to keep the generated Swift files separate from any hand-written ones. To see the output on the terminal rather than writing files to disk, pass `-` for this option. - -Finally, the command line should contain the `Java2Swift.config` file containing the list of classes that should be translated into Swift and their corresponding Swift type names. The tool will output a single `.swift` file for each class, along with warnings for any public API that cannot be translated into Swift. The most common warnings are due to missing Swift projections for Java classes. For example, here we have not translated (or provided the translation manifests for) the Java classes -`java.util.zip.ZipOutputStream` and `java.io.OutputStream`: - -``` -warning: Unable to translate 'java.util.jar.JarOutputStream' superclass: Java class 'java.util.zip.ZipOutputStream' has not been translated into Swift -warning: Unable to translate 'java.util.jar.JarOutputStream' constructor: Java class 'java.io.OutputStream' has not been translated into Swift -warning: Unable to translate 'java.util.jar.JarInputStream' method 'transferTo': Java class 'java.io.OutputStream' has not been translated into Swift -``` - -The result of such warnings is that certain information won't be statically available in Swift, e.g., the superclass won't be known (so we will assume it is `JavaObject`), or the specified constructors or methods won't be translated. If you don't need these APIs, the warnings can be safely ignored. The APIs can still be called dynamically via JNI. - -The `--jar` option changes the operation of `Java2Swift`. Instead of wrapping Java classes in Swift, it scans the given input Jar file to find all public classes and outputs a configuration file `Java2Swift.config` mapping all of the Java classes in the Jar file to Swift types. The `--jar` mode is expected to be used to help import a Java library into Swift wholesale, after which Java2Swift should invoked again given the generated configuration file. - -### Under construction: Create a Java class to wrap the Swift library - -**NOTE**: the instructions here work, but we are still smoothing out the interoperability story. - -All JavaKit-based applications start execution within the Java Virtual Machine. First, define your own Java class that loads your native Swift library and provides a `native` entry point to get into the Swift code. Here is a minimal Java class that has all of the program's logic written in Swift, including `main`: - - -```java -package org.swift.javakit; - -public class HelloSwiftMain { - static { - System.loadLibrary("HelloSwift"); - } - - public native static void main(String[] args); -} -``` - -Compile this into a `.class` file with `javac` before we build the Swift half, e.g.,: - -``` -javac Java/src/org/swift/javakit/JavaClassTranslator.java -``` - -### Create a Swift library - -The Java class created above loads a native library `HelloSwift` that needs to contain a definition of the `main` method in the class `org.swift.javakit.HelloSwiftMain`. `HelloSwift` should be defined as a SwiftPM dynamic library product, e.g., - -```swift - products: [ - .library( - name: "HelloSwift", - type: .dynamic, - targets: ["HelloSwift"] - ), - ] -``` - -with an associated target that depends on `JavaKit`: - -```swift - .target( - name: "HelloSwift", - dependencies: [ - .product(name: "ArgumentParser", package: "swift-argument-parser"), - .product(name: "JavaKit", package: "JavaKit") - ]) -``` - -### Implement the `native` Java method in Swift -Now, in the `HelloSwift` Swift library, define a `struct` that provides the `main` method for the Java class we already defined: - -```swift -import JavaKit - -@JavaImplementation("org.swift.javakit.HelloSwiftMain") -struct HelloSwiftMain { - @JavaStaticMethod - static func main(arguments: [String], environment: JNIEnvironment? = nil) { - print("Command line arguments are: \(arguments)") - } -} -``` - -Go ahead and build this library with `swift build`, and find the path to the directory containing the resulting shared library (e.g., `HelloSwift.dylib`, `HelloSwift.so`, or `HelloSwift.dll`, depending on platform). It is often in `.build/debug/` if you ran `swift build` on the command line. - -### Putting it all together! - -Finally, run this program on the command line like this: - -``` -java -cp Java/src -Djava.library.path=$(PATH_CONTAINING_HELLO_SWIFT)/ org.swift.javakit.HelloSwiftMain -v argument -``` - -This will prints the command-line arguments `-v` and `argument` as seen by Swift. - -### Bonus: Swift argument parser - -The easiest way to build a command-line program in Swift is with the [Swift argument parser library](https://github.com/apple/swift-argument-parser). We can extend our `HelloSwiftMain` type to conform to `ParsableCommand` and using the Swift argument parser to process the arguments provided by Java: - -```swift -import ArgumentParser -import JavaKit - -@JavaClass("org.swift.javakit.HelloSwiftMain") -struct HelloSwiftMain: ParsableCommand { - @Option(name: .shortAndLong, help: "Enable verbose output") - var verbose: Bool = false - - @JavaImplementation - static func main(arguments: [String], environment: JNIEnvironment? = nil) { - let command = Self.parseOrExit(arguments) - command.run(environment: environment) - } - - func run(environment: JNIEnvironment? = nil) { - print("Verbose = \(verbose)") - } -} -``` - -# `jextract-swift` - -The project is still very early days, however the general outline of using this approach is as follows: - -- **No code changes** need to be made to Swift libraries that are to be exposed to Java using jextract-swift. -- Swift sources are compiled to `.swiftinterface` files -- These `.swiftinterface` files are imported by jextract-swift which generates `*.java` files -- The generated Java files contain generated code for efficient native invocations. - -You can then use Swift libraries in Java just by calling the apropriate methods and initializers. - -## `jextract-swift`: Generating Java bridging files - -This repository also includes the `jextract-swift` tool which is similar to the JDK's [`jextract`](https://github.com/openjdk/jextract/). - -This approach is using Java's most recent (stable in JDK22) Foreign function and Memory APIs, collectively known as "Project Panama". You can read more about it here: https://openjdk.org/projects/panama/ It promises much higher performance than traditional approaches using JNI, and is primarily aimed for calling native code from a Java application. - -:warning: This feature requires JDK 22. The recommended way to install/manage JDKs is using [sdkman](https://sdkman.io): - -``` -curl -s "https://get.sdkman.io" | bash -sdk install java 22-open - -export JAVA_HOME=$(sdk home java 22-open) -``` - -`jextract-swift` can be pointed at `*.swiftinterface` files and will generate corresponding Java files that use the (new in Java 22) Foreign Function & Memory APIs to expose efficient ways to call "down" into Swift from Java. - -## JExtract: Swift <-> Java Type mapping - -TODO: these are not implemented yet. - -### Closures and Callbacks - -A Swift function may accept a closure which is used as a callback: - -```swift -func callMe(maybe: () -> ()) {} -``` - - - -## `jextract-swift` importer behavior - -Only `public` functions, properties and types are imported. - -Global Swift functions become static functions on on a class with the same name as the Swift module in Java, - -```swift -// Swift (Sources/SomeModule/Example.swift) - -public func globalFunction() -``` - -becomes: - -```java -// Java (SomeModule.java) - -public final class SomeModule ... { - public static void globalFunction() { ... } -} -``` diff --git a/Sources/JavaKit/Exceptions/Exception+Error.swift b/Sources/SwiftJava/Exceptions/Exception+Error.swift similarity index 100% rename from Sources/JavaKit/Exceptions/Exception+Error.swift rename to Sources/SwiftJava/Exceptions/Exception+Error.swift diff --git a/Sources/JavaKit/Exceptions/ExceptionHandling.swift b/Sources/SwiftJava/Exceptions/ExceptionHandling.swift similarity index 95% rename from Sources/JavaKit/Exceptions/ExceptionHandling.swift rename to Sources/SwiftJava/Exceptions/ExceptionHandling.swift index 02537281b..d5ca11078 100644 --- a/Sources/JavaKit/Exceptions/ExceptionHandling.swift +++ b/Sources/SwiftJava/Exceptions/ExceptionHandling.swift @@ -39,7 +39,7 @@ extension JNIEnvironment { } // Otherwise, create a exception with a message. - _ = try! JavaClass.withJNIClass(in: self) { exceptionClass in + _ = try! Exception.withJNIClass(in: self) { exceptionClass in interface.ThrowNew(self, exceptionClass, String(describing: error)) } } diff --git a/Sources/JavaKit/Exceptions/Throwable+Error.swift b/Sources/SwiftJava/Exceptions/Throwable+Error.swift similarity index 100% rename from Sources/JavaKit/Exceptions/Throwable+Error.swift rename to Sources/SwiftJava/Exceptions/Throwable+Error.swift diff --git a/Sources/JavaKit/JavaKitVM/JavaVirtualMachine.swift b/Sources/SwiftJava/JVM/JavaVirtualMachine.swift similarity index 91% rename from Sources/JavaKit/JavaKitVM/JavaVirtualMachine.swift rename to Sources/SwiftJava/JVM/JavaVirtualMachine.swift index 0800a89e2..bb574c8ad 100644 --- a/Sources/JavaKit/JavaKitVM/JavaVirtualMachine.swift +++ b/Sources/SwiftJava/JVM/JavaVirtualMachine.swift @@ -19,6 +19,19 @@ import Foundation #endif public typealias JavaVMPointer = UnsafeMutablePointer +#if canImport(Android) +typealias JNIEnvPointer = UnsafeMutablePointer +#else +typealias JNIEnvPointer = UnsafeMutableRawPointer +#endif + +extension FileManager { +#if os(Windows) + static let pathSeparator = ";" +#else + static let pathSeparator = ":" +#endif +} public final class JavaVirtualMachine: @unchecked Sendable { /// The JNI version that we depend on. @@ -61,7 +74,7 @@ public final class JavaVirtualMachine: @unchecked Sendable { ) throws { self.classpath = classpath var jvm: JavaVMPointer? = nil - var environment: UnsafeMutableRawPointer? = nil + var environment: JNIEnvPointer? = nil var vmArgs = JavaVMInitArgs() vmArgs.version = JavaVirtualMachine.jniVersion vmArgs.ignoreUnrecognized = jboolean(ignoreUnrecognized ? JNI_TRUE : JNI_FALSE) @@ -76,8 +89,8 @@ public final class JavaVirtualMachine: @unchecked Sendable { print("[warning][swift-java][JavaVirtualMachine] Missing classpath element: \(URL(fileURLWithPath: path).absoluteString)") // TODO: stderr } } - let colonSeparatedClassPath = classpath.joined(separator: ":") - allVMOptions.append("-Djava.class.path=\(colonSeparatedClassPath)") + let pathSeparatedClassPath = classpath.joined(separator: FileManager.pathSeparator) + allVMOptions.append("-Djava.class.path=\(pathSeparatedClassPath)") } allVMOptions.append(contentsOf: vmOptions) @@ -161,23 +174,33 @@ extension JavaVirtualMachine { return environment.assumingMemoryBound(to: JNIEnv?.self) } +#if canImport(Android) + var jniEnv = environment?.assumingMemoryBound(to: JNIEnv?.self) +#else + var jniEnv = environment +#endif + // Attach the current thread to the JVM. let attachResult: jint if asDaemon { - attachResult = jvm.pointee!.pointee.AttachCurrentThreadAsDaemon(jvm, &environment, nil) + attachResult = jvm.pointee!.pointee.AttachCurrentThreadAsDaemon(jvm, &jniEnv, nil) } else { - attachResult = jvm.pointee!.pointee.AttachCurrentThread(jvm, &environment, nil) + attachResult = jvm.pointee!.pointee.AttachCurrentThread(jvm, &jniEnv, nil) } // If we failed to attach, report that. if let attachError = VMError(fromJNIError: attachResult) { + // throw attachError fatalError("JVM Error: \(attachError)") - throw attachError } - JavaVirtualMachine.destroyTLS.set(environment!) + JavaVirtualMachine.destroyTLS.set(jniEnv!) - return environment!.assumingMemoryBound(to: JNIEnv?.self) +#if canImport(Android) + return jniEnv! +#else + return jniEnv!.assumingMemoryBound(to: JNIEnv?.self) +#endif } /// Detach the current thread from the Java Virtual Machine. All Java @@ -222,7 +245,7 @@ extension JavaVirtualMachine { ignoreUnrecognized: Bool = false, replace: Bool = false ) throws -> JavaVirtualMachine { - precondition(!classpath.contains(where: { $0.contains(":") }), "Classpath element must not contain `:`! Split the path into elements! Was: \(classpath)") + precondition(!classpath.contains(where: { $0.contains(FileManager.pathSeparator) }), "Classpath element must not contain `\(FileManager.pathSeparator)`! Split the path into elements! Was: \(classpath)") return try sharedJVM.withLock { (sharedJVMPointer: inout JavaVirtualMachine?) in // If we already have a JavaVirtualMachine instance, return it. diff --git a/Sources/JavaKit/JavaKitVM/LockedState.swift b/Sources/SwiftJava/JVM/LockedState.swift similarity index 99% rename from Sources/JavaKit/JavaKitVM/LockedState.swift rename to Sources/SwiftJava/JVM/LockedState.swift index e095668ca..b3082efcb 100644 --- a/Sources/JavaKit/JavaKitVM/LockedState.swift +++ b/Sources/SwiftJava/JVM/LockedState.swift @@ -13,9 +13,9 @@ //===----------------------------------------------------------------------===// #if canImport(os) -internal import os +import os #if FOUNDATION_FRAMEWORK && canImport(C.os.lock) -internal import C.os.lock +import C.os.lock #endif #elseif canImport(Bionic) import Bionic diff --git a/Sources/JavaKit/JavaKitVM/ThreadLocalStorage.swift b/Sources/SwiftJava/JVM/ThreadLocalStorage.swift similarity index 98% rename from Sources/JavaKit/JavaKitVM/ThreadLocalStorage.swift rename to Sources/SwiftJava/JVM/ThreadLocalStorage.swift index 7ea0b50a8..037e328b7 100644 --- a/Sources/JavaKit/JavaKitVM/ThreadLocalStorage.swift +++ b/Sources/SwiftJava/JVM/ThreadLocalStorage.swift @@ -56,7 +56,7 @@ package struct ThreadLocalStorage: ~Copyable { _key = 0 pthread_key_create(&_key, onThreadExit) #elseif canImport(WinSDK) - key = FlsAlloc(onThreadExit) + _key = FlsAlloc(onThreadExit) #endif } diff --git a/Sources/SwiftJava/JavaClass+CustomStringConvertible.swift b/Sources/SwiftJava/JavaClass+CustomStringConvertible.swift new file mode 100644 index 000000000..e7eb25105 --- /dev/null +++ b/Sources/SwiftJava/JavaClass+CustomStringConvertible.swift @@ -0,0 +1,21 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import CSwiftJavaJNI + +extension JavaClass: CustomStringConvertible { + public var description: String { + toString() + } +} \ No newline at end of file diff --git a/Sources/JavaKit/JavaClass+Initialization.swift b/Sources/SwiftJava/JavaClass+Initialization.swift similarity index 98% rename from Sources/JavaKit/JavaClass+Initialization.swift rename to Sources/SwiftJava/JavaClass+Initialization.swift index d443120f6..89bfa62b8 100644 --- a/Sources/JavaKit/JavaClass+Initialization.swift +++ b/Sources/SwiftJava/JavaClass+Initialization.swift @@ -12,7 +12,7 @@ // //===----------------------------------------------------------------------===// -import JavaRuntime +import CSwiftJavaJNI extension JavaClass { public typealias ObjectType = T diff --git a/Sources/JavaKit/JavaEnvironment.swift b/Sources/SwiftJava/JavaEnvironment.swift similarity index 75% rename from Sources/JavaKit/JavaEnvironment.swift rename to Sources/SwiftJava/JavaEnvironment.swift index d74146abd..9506dbead 100644 --- a/Sources/JavaKit/JavaEnvironment.swift +++ b/Sources/SwiftJava/JavaEnvironment.swift @@ -12,8 +12,12 @@ // //===----------------------------------------------------------------------===// -import JavaRuntime +import CSwiftJavaJNI + +#if canImport(Android) +public typealias JNINativeInterface_ = JNINativeInterface +#endif extension UnsafeMutablePointer { - var interface: JNINativeInterface_ { self.pointee!.pointee } + public var interface: JNINativeInterface_ { self.pointee!.pointee } } diff --git a/Sources/JavaKit/JavaObject+Inheritance.swift b/Sources/SwiftJava/JavaObject+Inheritance.swift similarity index 79% rename from Sources/JavaKit/JavaObject+Inheritance.swift rename to Sources/SwiftJava/JavaObject+Inheritance.swift index 43d86c2ae..0ddd79449 100644 --- a/Sources/JavaKit/JavaObject+Inheritance.swift +++ b/Sources/SwiftJava/JavaObject+Inheritance.swift @@ -12,7 +12,7 @@ // //===----------------------------------------------------------------------===// -import JavaRuntime +import CSwiftJavaJNI extension AnyJavaObject { /// Look up the other class type @@ -22,16 +22,16 @@ extension AnyJavaObject { private func isInstanceOf( _ otherClass: OtherClass.Type ) -> jclass? { - try? otherClass.withJNIClass(in: javaEnvironment) { otherJavaClass in - if javaEnvironment.interface.IsInstanceOf( - javaEnvironment, - javaThis, - otherJavaClass - ) == 0 { - return nil - } + guard let this: jobject = javaThisOptional else { + return nil + } + + return try? otherClass.withJNIClass(in: javaEnvironment) { otherJavaClass in + if javaEnvironment.interface.IsInstanceOf(javaEnvironment, this, otherJavaClass) == 0 { + return nil + } - return otherJavaClass + return otherJavaClass } } diff --git a/Sources/JavaKit/JavaObject+MethodCalls.swift b/Sources/SwiftJava/JavaObject+MethodCalls.swift similarity index 89% rename from Sources/JavaKit/JavaObject+MethodCalls.swift rename to Sources/SwiftJava/JavaObject+MethodCalls.swift index 9f02a58d2..0626be23a 100644 --- a/Sources/JavaKit/JavaObject+MethodCalls.swift +++ b/Sources/SwiftJava/JavaObject+MethodCalls.swift @@ -12,7 +12,7 @@ // //===----------------------------------------------------------------------===// -import JavaRuntime +import CSwiftJavaJNI import JavaTypes /// Produce the mangling for a method with the given argument and result types. @@ -104,7 +104,8 @@ extension AnyJavaObject { resultType: Result.Type ) throws -> jmethodID { // Retrieve the Java class instance from the object. - let environment = javaEnvironment + let environment = try JavaVirtualMachine.shared().environment() + let thisClass = try environment.translatingJNIExceptions { environment.interface.GetObjectClass(environment, javaThis) }! @@ -115,7 +116,7 @@ extension AnyJavaObject { methodName: methodName, parameterTypes: repeat each parameterTypes, resultType: Result.javaType, - in: javaEnvironment + in: environment ) } } @@ -126,7 +127,8 @@ extension AnyJavaObject { parameterTypes: repeat (each Param).Type ) throws -> jmethodID { // Retrieve the Java class instance from the object. - let environment = javaEnvironment + let environment = try JavaVirtualMachine.shared().environment() + let thisClass = try environment.translatingJNIExceptions { environment.interface.GetObjectClass(environment, javaThis) }! @@ -137,7 +139,7 @@ extension AnyJavaObject { methodName: methodName, parameterTypes: repeat each parameterTypes, resultType: .void, - in: javaEnvironment + in: environment ) } } @@ -167,8 +169,10 @@ extension AnyJavaObject { method: jmethodID, args: repeat each Param ) throws -> Result { + let environment = try JavaVirtualMachine.shared().environment() + return try Self.javaMethodCall( - in: javaEnvironment, + in: environment, this: javaThis, method: method, args: repeat each args @@ -229,8 +233,10 @@ extension AnyJavaObject { method: jmethodID, args: repeat each Param ) throws { + let environment = try JavaVirtualMachine.shared().environment() + try Self.javaMethodCall( - in: javaEnvironment, + in: environment, this: javaThis, method: method, args: repeat each args @@ -276,7 +282,7 @@ extension AnyJavaObject { private func getJNIFieldID(_ fieldName: String, fieldType: FieldType.Type) -> jfieldID? where FieldType: ~Copyable { let this = javaThis - let environment = javaEnvironment + let environment = try! JavaVirtualMachine.shared().environment() // Retrieve the Java class instance from the object. let thisClass = environment.interface.GetObjectClass(environment, this)! @@ -289,15 +295,19 @@ extension AnyJavaObject { fieldType fieldType: FieldType.Type ) -> FieldType where FieldType: ~Copyable { get { + let environment = try! JavaVirtualMachine.shared().environment() + let fieldID = getJNIFieldID(fieldName, fieldType: fieldType)! - let jniMethod = FieldType.jniFieldGet(in: javaEnvironment) - return FieldType(fromJNI: jniMethod(javaEnvironment, javaThis, fieldID), in: javaEnvironment) + let jniMethod = FieldType.jniFieldGet(in: environment) + return FieldType(fromJNI: jniMethod(environment, javaThis, fieldID), in: environment) } nonmutating set { + let environment = try! JavaVirtualMachine.shared().environment() + let fieldID = getJNIFieldID(fieldName, fieldType: fieldType)! - let jniMethod = FieldType.jniFieldSet(in: javaEnvironment) - jniMethod(javaEnvironment, javaThis, fieldID, newValue.getJNIValue(in: javaEnvironment)) + let jniMethod = FieldType.jniFieldSet(in: environment) + jniMethod(environment, javaThis, fieldID, newValue.getJNIValue(in: environment)) } } } @@ -311,7 +321,7 @@ extension JavaClass { resultType: Result.Type ) throws -> Result { let thisClass = javaThis - let environment = javaEnvironment + let environment = try! JavaVirtualMachine.shared().environment() // Compute the method signature so we can find the right method, then look up the // method within the class. @@ -345,7 +355,7 @@ extension JavaClass { arguments: repeat each Param ) throws { let thisClass = javaThis - let environment = javaEnvironment + let environment = try JavaVirtualMachine.shared().environment() // Compute the method signature so we can find the right method, then look up the // method within the class. @@ -372,7 +382,7 @@ extension JavaClass { /// Retrieve the JNI field ID for a field with the given name and type. private func getJNIStaticFieldID(_ fieldName: String, fieldType: FieldType.Type) -> jfieldID? { - let environment = javaEnvironment + let environment = try! JavaVirtualMachine.shared().environment() return environment.interface.GetStaticFieldID(environment, javaThis, fieldName, FieldType.jniMangling) } @@ -382,15 +392,19 @@ extension JavaClass { fieldType fieldType: FieldType.Type ) -> FieldType { get { + let environment = try! JavaVirtualMachine.shared().environment() + let fieldID = getJNIStaticFieldID(fieldName, fieldType: fieldType)! - let jniMethod = FieldType.jniStaticFieldGet(in: javaEnvironment) - return FieldType(fromJNI: jniMethod(javaEnvironment, javaThis, fieldID), in: javaEnvironment) + let jniMethod = FieldType.jniStaticFieldGet(in: environment) + return FieldType(fromJNI: jniMethod(environment, javaThis, fieldID), in: environment) } set { + let environment = try! JavaVirtualMachine.shared().environment() + let fieldID = getJNIStaticFieldID(fieldName, fieldType: fieldType)! - let jniMethod = FieldType.jniStaticFieldSet(in: javaEnvironment) - jniMethod(javaEnvironment, javaThis, fieldID, newValue.getJNIValue(in: javaEnvironment)) + let jniMethod = FieldType.jniStaticFieldSet(in: environment) + jniMethod(environment, javaThis, fieldID, newValue.getJNIValue(in: environment)) } } } diff --git a/Sources/JavaKit/JavaObjectHolder.swift b/Sources/SwiftJava/JavaObjectHolder.swift similarity index 87% rename from Sources/JavaKit/JavaObjectHolder.swift rename to Sources/SwiftJava/JavaObjectHolder.swift index 173991c98..b5e888351 100644 --- a/Sources/JavaKit/JavaObjectHolder.swift +++ b/Sources/SwiftJava/JavaObjectHolder.swift @@ -12,13 +12,13 @@ // //===----------------------------------------------------------------------===// -import JavaRuntime +import CSwiftJavaJNI /// Stores a reference to a Java object, managing it as a global reference so /// that the Java virtual machine will not move or deallocate the object /// while this instance is live. -public class JavaObjectHolder { - public private(set) var object: jobject? +public final class JavaObjectHolder { + public private(set) var object: jobject? // TODO: thread-safety public let environment: JNIEnvironment /// Take a reference to a Java object and promote it to a global reference @@ -32,6 +32,8 @@ public class JavaObjectHolder { /// in Swift and the Java virtual machine is free to move or deallocate it. func forget() { if let object { + let environment = try! JavaVirtualMachine.shared().environment() + environment.interface.DeleteGlobalRef(environment, object) self.object = nil } diff --git a/Sources/SwiftJava/JavaString+Extensions.swift b/Sources/SwiftJava/JavaString+Extensions.swift new file mode 100644 index 000000000..1836777d2 --- /dev/null +++ b/Sources/SwiftJava/JavaString+Extensions.swift @@ -0,0 +1,34 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import CSwiftJavaJNI +import JavaTypes + +extension JavaString: CustomStringConvertible, CustomDebugStringConvertible { + public var description: String { + return toString() + } + public var debugDescription: String { + return "\"" + toString() + "\"" + } +} + +extension Optional where Wrapped == JavaString { + public var description: String { + switch self { + case .some(let value): "Optional(\(value.toString())" + case .none: "nil" + } + } +} \ No newline at end of file diff --git a/Sources/JavaKit/JavaValue.swift b/Sources/SwiftJava/JavaValue.swift similarity index 97% rename from Sources/JavaKit/JavaValue.swift rename to Sources/SwiftJava/JavaValue.swift index 310b54df3..46efdb3fc 100644 --- a/Sources/JavaKit/JavaValue.swift +++ b/Sources/SwiftJava/JavaValue.swift @@ -12,7 +12,7 @@ // //===----------------------------------------------------------------------===// -import JavaRuntime +import CSwiftJavaJNI import JavaTypes /// Describes a type that can be bridged with Java. @@ -34,7 +34,7 @@ import JavaTypes /// The protocol provides operations to bridge values in both directions: /// - `getJNIValue(in:)`: produces the JNI value (of type `JNIType`) for the /// `self` Swift value in the given JNI environment. -/// - `init(fromJNI:in:)`: intializes a Swift value from the JNI value (of +/// - `init(fromJNI:in:)`: initializes a Swift value from the JNI value (of /// type `JNIType`) in the given JNI environment. /// /// The protocol also provides hooks to tie into JNI, including operations to @@ -137,7 +137,7 @@ extension JavaValue where Self: ~Copyable { } /// Convert to a jvalue within the given JNI environment. - consuming func getJValue(in environment: JNIEnvironment) -> jvalue { + public consuming func getJValue(in environment: JNIEnvironment) -> jvalue { var result = jvalue() Self.assignJNIType(&result[keyPath: Self.jvalueKeyPath], to: getJNIValue(in: environment)) return result diff --git a/Sources/JavaKit/Macros.swift b/Sources/SwiftJava/Macros.swift similarity index 75% rename from Sources/JavaKit/Macros.swift rename to Sources/SwiftJava/Macros.swift index 029344d84..4c0353661 100644 --- a/Sources/JavaKit/Macros.swift +++ b/Sources/SwiftJava/Macros.swift @@ -43,7 +43,7 @@ public macro JavaClass( _ fullClassName: String, extends: (any AnyJavaObject.Type)? = nil, implements: (any AnyJavaObject.Type)?... -) = #externalMacro(module: "JavaKitMacros", type: "JavaClassMacro") +) = #externalMacro(module: "SwiftJavaMacros", type: "JavaClassMacro") /// Attached macro that declares that a particular `struct` type is a wrapper around a Java interface. /// @@ -74,7 +74,7 @@ public macro JavaClass( ) @attached(extension, conformances: AnyJavaObject) public macro JavaInterface(_ fullClassName: String, extends: (any AnyJavaObject.Type)?...) = - #externalMacro(module: "JavaKitMacros", type: "JavaClassMacro") + #externalMacro(module: "SwiftJavaMacros", type: "JavaClassMacro") /// Attached macro that turns a Swift property into one that accesses a Java field on the underlying Java object. /// @@ -88,7 +88,7 @@ public macro JavaInterface(_ fullClassName: String, extends: (any AnyJavaObject. /// } /// ``` @attached(accessor) -public macro JavaField(_ javaFieldName: String? = nil, isFinal: Bool = false) = #externalMacro(module: "JavaKitMacros", type: "JavaFieldMacro") +public macro JavaField(_ javaFieldName: String? = nil, isFinal: Bool = false) = #externalMacro(module: "SwiftJavaMacros", type: "JavaFieldMacro") /// Attached macro that turns a Swift property into one that accesses a Java static field on the underlying Java object. @@ -102,7 +102,7 @@ public macro JavaField(_ javaFieldName: String? = nil, isFinal: Bool = false) = /// } /// ``` @attached(accessor) -public macro JavaStaticField(_ javaFieldName: String? = nil, isFinal: Bool = false) = #externalMacro(module: "JavaKitMacros", type: "JavaFieldMacro") +public macro JavaStaticField(_ javaFieldName: String? = nil, isFinal: Bool = false) = #externalMacro(module: "SwiftJavaMacros", type: "JavaFieldMacro") /// Attached macro that turns a Swift method into one that wraps a Java method on the underlying Java object. /// @@ -123,8 +123,27 @@ public macro JavaStaticField(_ javaFieldName: String? = nil, isFinal: Bool = fal /// ``` /// /// corresponds to the Java constructor `HelloSwift(String name)`. +/// +/// ### Generics and type-erasure +/// Swift and Java differ in how they represent generics at runtime. +/// In Java, generics are type-erased and the JVM representation of generic types is erased to `java.lang.Object`. +/// Swift on the other hand, reifies types which means a `Test` in practice will be a specific type with +/// the generic substituted `Test`. This means that at runtime, calling a generic @JavaMethod needs to know +/// which of the parameters (or result type) must be subjected to type-erasure as we form the call into the Java function. +/// +/// In order to mark a generic return type you must indicate it to the @JavaMethod macro like this: +/// ```swift +/// // Java: class Test { public get(); } +/// @JavaMethod(typeErasedResult: "T!") +/// func get() -> T! +/// ``` +/// This allows the macro to form a call into the get() method, which at runtime, will have an `java.lang.Object` +/// returning method signature, and then, convert the result to the expected `T` type on the Swift side. @attached(body) -public macro JavaMethod() = #externalMacro(module: "JavaKitMacros", type: "JavaMethodMacro") +public macro JavaMethod( + _ javaMethodName: String? = nil, + typeErasedResult: String? = nil +) = #externalMacro(module: "SwiftJavaMacros", type: "JavaMethodMacro") /// Attached macro that turns a Swift method on JavaClass into one that wraps /// a Java static method on the underlying Java class object. @@ -132,11 +151,11 @@ public macro JavaMethod() = #externalMacro(module: "JavaKitMacros", type: "JavaM /// The macro must be used within a specific JavaClass instance. /// /// ```swift -/// @JavaMethod +/// @JavaStaticMethod /// func sayHelloBack(_ i: Int32) -> Double /// ``` @attached(body) -public macro JavaStaticMethod() = #externalMacro(module: "JavaKitMacros", type: "JavaMethodMacro") +public macro JavaStaticMethod(_ javaMethodName: String? = nil) = #externalMacro(module: "SwiftJavaMacros", type: "JavaMethodMacro") /// Macro that marks extensions to specify that all of the @JavaMethod /// methods are implementations of Java methods spelled as `native`. @@ -161,4 +180,4 @@ public macro JavaStaticMethod() = #externalMacro(module: "JavaKitMacros", type: /// } /// ``` @attached(peer) -public macro JavaImplementation(_ fullClassName: String) = #externalMacro(module: "JavaKitMacros", type: "JavaImplementationMacro") +public macro JavaImplementation(_ fullClassName: String) = #externalMacro(module: "SwiftJavaMacros", type: "JavaImplementationMacro") diff --git a/Sources/JavaKit/Optional+JavaObject.swift b/Sources/SwiftJava/Optional+JavaObject.swift similarity index 99% rename from Sources/JavaKit/Optional+JavaObject.swift rename to Sources/SwiftJava/Optional+JavaObject.swift index 7aff42942..46fd99709 100644 --- a/Sources/JavaKit/Optional+JavaObject.swift +++ b/Sources/SwiftJava/Optional+JavaObject.swift @@ -12,7 +12,7 @@ // //===----------------------------------------------------------------------===// -import JavaRuntime +import CSwiftJavaJNI import JavaTypes extension Optional: JavaValue where Wrapped: AnyJavaObject { diff --git a/Sources/JavaKit/Optional+JavaOptional.swift b/Sources/SwiftJava/Optional+JavaOptional.swift similarity index 85% rename from Sources/JavaKit/Optional+JavaOptional.swift rename to Sources/SwiftJava/Optional+JavaOptional.swift index c9becd584..8e3924fb1 100644 --- a/Sources/JavaKit/Optional+JavaOptional.swift +++ b/Sources/SwiftJava/Optional+JavaOptional.swift @@ -79,3 +79,17 @@ public extension Optional where Wrapped == Int64 { } } } + +extension JavaOptional { + public func empty(environment: JNIEnvironment? = nil) -> JavaOptional! { + guard let env = try? environment ?? JavaVirtualMachine.shared().environment() else { + return nil + } + + guard let opt = try? JavaClass>(environment: env).empty() else { + return nil + } + + return opt.as(JavaOptional.self) + } +} diff --git a/Sources/SwiftJavaTool/String+Extensions.swift b/Sources/SwiftJava/String+Extensions.swift similarity index 72% rename from Sources/SwiftJavaTool/String+Extensions.swift rename to Sources/SwiftJava/String+Extensions.swift index f2bb9e729..b87e6a0c6 100644 --- a/Sources/SwiftJavaTool/String+Extensions.swift +++ b/Sources/SwiftJava/String+Extensions.swift @@ -13,17 +13,10 @@ //===----------------------------------------------------------------------===// import Foundation -import ArgumentParser -import SwiftJavaLib -import JavaKit -import JavaKitJar -import SwiftJavaLib -import JavaKitConfigurationShared extension String { - /// For a String that's of the form java.util.Vector, return the "Vector" - /// part. - var defaultSwiftNameForJavaClass: String { + /// For a String that's of the form java.util.Vector, return the "Vector" part. + package var defaultSwiftNameForJavaClass: String { if let dotLoc = lastIndex(of: ".") { let afterDot = index(after: dotLoc) return String(self[afterDot...]).javaClassNameToCanonicalName.adjustedSwiftTypeName @@ -34,14 +27,18 @@ extension String { } extension String { - /// Replace all of the $'s for nested names with "." to turn a Java class - /// name into a Java canonical class name, - var javaClassNameToCanonicalName: String { - return replacing("$", with: ".") + /// Convert a Java class name to its canonical name. + /// Replaces `$` with `.` for nested classes but preserves `$` at the start of identifiers. + package var javaClassNameToCanonicalName: String { + self.replacingOccurrences( + of: #"(?<=\w)\$"#, + with: ".", + options: .regularExpression + ) } /// Whether this is the name of an anonymous class. - var isLocalJavaClass: Bool { + package var isLocalJavaClass: Bool { for segment in split(separator: "$") { if let firstChar = segment.first, firstChar.isNumber { return true @@ -52,7 +49,7 @@ extension String { } /// Adjust type name for "bad" type names that don't work well in Swift. - var adjustedSwiftTypeName: String { + package var adjustedSwiftTypeName: String { switch self { case "Type": return "JavaType" default: return self diff --git a/Samples/SwiftKitSampleApp/Sources/MySwiftLibrary/_RuntimeWorkarounds.swift b/Sources/SwiftJava/SwiftJavaConversionError.swift similarity index 65% rename from Samples/SwiftKitSampleApp/Sources/MySwiftLibrary/_RuntimeWorkarounds.swift rename to Sources/SwiftJava/SwiftJavaConversionError.swift index f3056973e..5b29741ce 100644 --- a/Samples/SwiftKitSampleApp/Sources/MySwiftLibrary/_RuntimeWorkarounds.swift +++ b/Sources/SwiftJava/SwiftJavaConversionError.swift @@ -12,11 +12,11 @@ // //===----------------------------------------------------------------------===// -#if os(Linux) -// FIXME: why do we need this workaround? -@_silgen_name("_objc_autoreleaseReturnValue") -public func _objc_autoreleaseReturnValue(a: Any) {} +/// Used to indicate Swift/Java conversion failures. +public struct SwiftJavaConversionError: Error { + public let message: String -@_silgen_name("objc_autoreleaseReturnValue") -public func objc_autoreleaseReturnValue(a: Any) {} -#endif + public init(_ message: String) { + self.message = message + } +} \ No newline at end of file diff --git a/Sources/JavaKit/generated/Appendable.swift b/Sources/SwiftJava/generated/Appendable.swift similarity index 94% rename from Sources/JavaKit/generated/Appendable.swift rename to Sources/SwiftJava/generated/Appendable.swift index 5c6663f27..b0c67ec50 100644 --- a/Sources/JavaKit/generated/Appendable.swift +++ b/Sources/SwiftJava/generated/Appendable.swift @@ -1,5 +1,5 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaRuntime +import CSwiftJavaJNI @JavaInterface("java.lang.Appendable") public struct Appendable { diff --git a/Sources/JavaKit/generated/CharSequence.swift b/Sources/SwiftJava/generated/CharSequence.swift similarity index 81% rename from Sources/JavaKit/generated/CharSequence.swift rename to Sources/SwiftJava/generated/CharSequence.swift index cab17273c..ee5dca369 100644 --- a/Sources/JavaKit/generated/CharSequence.swift +++ b/Sources/SwiftJava/generated/CharSequence.swift @@ -1,5 +1,5 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaRuntime +import CSwiftJavaJNI @JavaInterface("java.lang.CharSequence") public struct CharSequence { @@ -9,6 +9,9 @@ public struct CharSequence { @JavaMethod public func toString() -> String + @JavaMethod + public func getChars(_ arg0: Int32, _ arg1: Int32, _ arg2: [UInt16], _ arg3: Int32) + @JavaMethod public func charAt(_ arg0: Int32) -> UInt16 diff --git a/Sources/JavaKit/generated/Exception.swift b/Sources/SwiftJava/generated/Exception.swift similarity index 96% rename from Sources/JavaKit/generated/Exception.swift rename to Sources/SwiftJava/generated/Exception.swift index 6fc9de8c0..e87684cbd 100644 --- a/Sources/JavaKit/generated/Exception.swift +++ b/Sources/SwiftJava/generated/Exception.swift @@ -1,5 +1,5 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaRuntime +import CSwiftJavaJNI @JavaClass("java.lang.Exception") open class Exception: Throwable { diff --git a/Sources/JavaKit/generated/JavaArray.swift b/Sources/SwiftJava/generated/JavaArray.swift similarity index 99% rename from Sources/JavaKit/generated/JavaArray.swift rename to Sources/SwiftJava/generated/JavaArray.swift index 147f24df6..ae1822088 100644 --- a/Sources/JavaKit/generated/JavaArray.swift +++ b/Sources/SwiftJava/generated/JavaArray.swift @@ -1,5 +1,5 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaRuntime +import CSwiftJavaJNI @JavaClass("java.lang.reflect.Array") open class JavaArray: JavaObject { diff --git a/Sources/JavaKit/generated/JavaBoolean.swift b/Sources/SwiftJava/generated/JavaBoolean.swift similarity index 98% rename from Sources/JavaKit/generated/JavaBoolean.swift rename to Sources/SwiftJava/generated/JavaBoolean.swift index 0ab51f6d0..bdf21df90 100644 --- a/Sources/JavaKit/generated/JavaBoolean.swift +++ b/Sources/SwiftJava/generated/JavaBoolean.swift @@ -1,5 +1,5 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaRuntime +import CSwiftJavaJNI @JavaClass("java.lang.Boolean") open class JavaBoolean: JavaObject { diff --git a/Sources/JavaKit/generated/JavaByte.swift b/Sources/SwiftJava/generated/JavaByte.swift similarity index 99% rename from Sources/JavaKit/generated/JavaByte.swift rename to Sources/SwiftJava/generated/JavaByte.swift index e75c5996c..e3f67c783 100644 --- a/Sources/JavaKit/generated/JavaByte.swift +++ b/Sources/SwiftJava/generated/JavaByte.swift @@ -1,5 +1,5 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaRuntime +import CSwiftJavaJNI @JavaClass("java.lang.Byte") open class JavaByte: JavaNumber { diff --git a/Sources/JavaKit/generated/JavaCharacter.swift b/Sources/SwiftJava/generated/JavaCharacter.swift similarity index 92% rename from Sources/JavaKit/generated/JavaCharacter.swift rename to Sources/SwiftJava/generated/JavaCharacter.swift index eead7dfd9..f79742a3b 100644 --- a/Sources/JavaKit/generated/JavaCharacter.swift +++ b/Sources/SwiftJava/generated/JavaCharacter.swift @@ -1,5 +1,5 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaRuntime +import CSwiftJavaJNI @JavaClass("java.lang.Character") open class JavaCharacter: JavaObject { @@ -1557,985 +1557,985 @@ extension JavaCharacter { if let COMMON = classObj.COMMON { self.init(javaHolder: COMMON.javaHolder) } else { - fatalError("Enum value COMMON was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value COMMON was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .LATIN: if let LATIN = classObj.LATIN { self.init(javaHolder: LATIN.javaHolder) } else { - fatalError("Enum value LATIN was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value LATIN was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .GREEK: if let GREEK = classObj.GREEK { self.init(javaHolder: GREEK.javaHolder) } else { - fatalError("Enum value GREEK was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value GREEK was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .CYRILLIC: if let CYRILLIC = classObj.CYRILLIC { self.init(javaHolder: CYRILLIC.javaHolder) } else { - fatalError("Enum value CYRILLIC was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value CYRILLIC was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .ARMENIAN: if let ARMENIAN = classObj.ARMENIAN { self.init(javaHolder: ARMENIAN.javaHolder) } else { - fatalError("Enum value ARMENIAN was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value ARMENIAN was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .HEBREW: if let HEBREW = classObj.HEBREW { self.init(javaHolder: HEBREW.javaHolder) } else { - fatalError("Enum value HEBREW was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value HEBREW was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .ARABIC: if let ARABIC = classObj.ARABIC { self.init(javaHolder: ARABIC.javaHolder) } else { - fatalError("Enum value ARABIC was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value ARABIC was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .SYRIAC: if let SYRIAC = classObj.SYRIAC { self.init(javaHolder: SYRIAC.javaHolder) } else { - fatalError("Enum value SYRIAC was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value SYRIAC was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .THAANA: if let THAANA = classObj.THAANA { self.init(javaHolder: THAANA.javaHolder) } else { - fatalError("Enum value THAANA was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value THAANA was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .DEVANAGARI: if let DEVANAGARI = classObj.DEVANAGARI { self.init(javaHolder: DEVANAGARI.javaHolder) } else { - fatalError("Enum value DEVANAGARI was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value DEVANAGARI was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .BENGALI: if let BENGALI = classObj.BENGALI { self.init(javaHolder: BENGALI.javaHolder) } else { - fatalError("Enum value BENGALI was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value BENGALI was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .GURMUKHI: if let GURMUKHI = classObj.GURMUKHI { self.init(javaHolder: GURMUKHI.javaHolder) } else { - fatalError("Enum value GURMUKHI was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value GURMUKHI was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .GUJARATI: if let GUJARATI = classObj.GUJARATI { self.init(javaHolder: GUJARATI.javaHolder) } else { - fatalError("Enum value GUJARATI was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value GUJARATI was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .ORIYA: if let ORIYA = classObj.ORIYA { self.init(javaHolder: ORIYA.javaHolder) } else { - fatalError("Enum value ORIYA was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value ORIYA was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .TAMIL: if let TAMIL = classObj.TAMIL { self.init(javaHolder: TAMIL.javaHolder) } else { - fatalError("Enum value TAMIL was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value TAMIL was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .TELUGU: if let TELUGU = classObj.TELUGU { self.init(javaHolder: TELUGU.javaHolder) } else { - fatalError("Enum value TELUGU was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value TELUGU was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .KANNADA: if let KANNADA = classObj.KANNADA { self.init(javaHolder: KANNADA.javaHolder) } else { - fatalError("Enum value KANNADA was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value KANNADA was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .MALAYALAM: if let MALAYALAM = classObj.MALAYALAM { self.init(javaHolder: MALAYALAM.javaHolder) } else { - fatalError("Enum value MALAYALAM was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value MALAYALAM was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .SINHALA: if let SINHALA = classObj.SINHALA { self.init(javaHolder: SINHALA.javaHolder) } else { - fatalError("Enum value SINHALA was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value SINHALA was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .THAI: if let THAI = classObj.THAI { self.init(javaHolder: THAI.javaHolder) } else { - fatalError("Enum value THAI was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value THAI was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .LAO: if let LAO = classObj.LAO { self.init(javaHolder: LAO.javaHolder) } else { - fatalError("Enum value LAO was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value LAO was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .TIBETAN: if let TIBETAN = classObj.TIBETAN { self.init(javaHolder: TIBETAN.javaHolder) } else { - fatalError("Enum value TIBETAN was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value TIBETAN was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .MYANMAR: if let MYANMAR = classObj.MYANMAR { self.init(javaHolder: MYANMAR.javaHolder) } else { - fatalError("Enum value MYANMAR was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value MYANMAR was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .GEORGIAN: if let GEORGIAN = classObj.GEORGIAN { self.init(javaHolder: GEORGIAN.javaHolder) } else { - fatalError("Enum value GEORGIAN was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value GEORGIAN was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .HANGUL: if let HANGUL = classObj.HANGUL { self.init(javaHolder: HANGUL.javaHolder) } else { - fatalError("Enum value HANGUL was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value HANGUL was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .ETHIOPIC: if let ETHIOPIC = classObj.ETHIOPIC { self.init(javaHolder: ETHIOPIC.javaHolder) } else { - fatalError("Enum value ETHIOPIC was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value ETHIOPIC was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .CHEROKEE: if let CHEROKEE = classObj.CHEROKEE { self.init(javaHolder: CHEROKEE.javaHolder) } else { - fatalError("Enum value CHEROKEE was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value CHEROKEE was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .CANADIAN_ABORIGINAL: if let CANADIAN_ABORIGINAL = classObj.CANADIAN_ABORIGINAL { self.init(javaHolder: CANADIAN_ABORIGINAL.javaHolder) } else { - fatalError("Enum value CANADIAN_ABORIGINAL was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value CANADIAN_ABORIGINAL was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .OGHAM: if let OGHAM = classObj.OGHAM { self.init(javaHolder: OGHAM.javaHolder) } else { - fatalError("Enum value OGHAM was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value OGHAM was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .RUNIC: if let RUNIC = classObj.RUNIC { self.init(javaHolder: RUNIC.javaHolder) } else { - fatalError("Enum value RUNIC was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value RUNIC was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .KHMER: if let KHMER = classObj.KHMER { self.init(javaHolder: KHMER.javaHolder) } else { - fatalError("Enum value KHMER was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value KHMER was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .MONGOLIAN: if let MONGOLIAN = classObj.MONGOLIAN { self.init(javaHolder: MONGOLIAN.javaHolder) } else { - fatalError("Enum value MONGOLIAN was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value MONGOLIAN was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .HIRAGANA: if let HIRAGANA = classObj.HIRAGANA { self.init(javaHolder: HIRAGANA.javaHolder) } else { - fatalError("Enum value HIRAGANA was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value HIRAGANA was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .KATAKANA: if let KATAKANA = classObj.KATAKANA { self.init(javaHolder: KATAKANA.javaHolder) } else { - fatalError("Enum value KATAKANA was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value KATAKANA was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .BOPOMOFO: if let BOPOMOFO = classObj.BOPOMOFO { self.init(javaHolder: BOPOMOFO.javaHolder) } else { - fatalError("Enum value BOPOMOFO was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value BOPOMOFO was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .HAN: if let HAN = classObj.HAN { self.init(javaHolder: HAN.javaHolder) } else { - fatalError("Enum value HAN was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value HAN was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .YI: if let YI = classObj.YI { self.init(javaHolder: YI.javaHolder) } else { - fatalError("Enum value YI was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value YI was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .OLD_ITALIC: if let OLD_ITALIC = classObj.OLD_ITALIC { self.init(javaHolder: OLD_ITALIC.javaHolder) } else { - fatalError("Enum value OLD_ITALIC was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value OLD_ITALIC was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .GOTHIC: if let GOTHIC = classObj.GOTHIC { self.init(javaHolder: GOTHIC.javaHolder) } else { - fatalError("Enum value GOTHIC was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value GOTHIC was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .DESERET: if let DESERET = classObj.DESERET { self.init(javaHolder: DESERET.javaHolder) } else { - fatalError("Enum value DESERET was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value DESERET was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .INHERITED: if let INHERITED = classObj.INHERITED { self.init(javaHolder: INHERITED.javaHolder) } else { - fatalError("Enum value INHERITED was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value INHERITED was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .TAGALOG: if let TAGALOG = classObj.TAGALOG { self.init(javaHolder: TAGALOG.javaHolder) } else { - fatalError("Enum value TAGALOG was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value TAGALOG was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .HANUNOO: if let HANUNOO = classObj.HANUNOO { self.init(javaHolder: HANUNOO.javaHolder) } else { - fatalError("Enum value HANUNOO was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value HANUNOO was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .BUHID: if let BUHID = classObj.BUHID { self.init(javaHolder: BUHID.javaHolder) } else { - fatalError("Enum value BUHID was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value BUHID was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .TAGBANWA: if let TAGBANWA = classObj.TAGBANWA { self.init(javaHolder: TAGBANWA.javaHolder) } else { - fatalError("Enum value TAGBANWA was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value TAGBANWA was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .LIMBU: if let LIMBU = classObj.LIMBU { self.init(javaHolder: LIMBU.javaHolder) } else { - fatalError("Enum value LIMBU was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value LIMBU was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .TAI_LE: if let TAI_LE = classObj.TAI_LE { self.init(javaHolder: TAI_LE.javaHolder) } else { - fatalError("Enum value TAI_LE was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value TAI_LE was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .LINEAR_B: if let LINEAR_B = classObj.LINEAR_B { self.init(javaHolder: LINEAR_B.javaHolder) } else { - fatalError("Enum value LINEAR_B was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value LINEAR_B was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .UGARITIC: if let UGARITIC = classObj.UGARITIC { self.init(javaHolder: UGARITIC.javaHolder) } else { - fatalError("Enum value UGARITIC was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value UGARITIC was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .SHAVIAN: if let SHAVIAN = classObj.SHAVIAN { self.init(javaHolder: SHAVIAN.javaHolder) } else { - fatalError("Enum value SHAVIAN was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value SHAVIAN was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .OSMANYA: if let OSMANYA = classObj.OSMANYA { self.init(javaHolder: OSMANYA.javaHolder) } else { - fatalError("Enum value OSMANYA was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value OSMANYA was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .CYPRIOT: if let CYPRIOT = classObj.CYPRIOT { self.init(javaHolder: CYPRIOT.javaHolder) } else { - fatalError("Enum value CYPRIOT was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value CYPRIOT was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .BRAILLE: if let BRAILLE = classObj.BRAILLE { self.init(javaHolder: BRAILLE.javaHolder) } else { - fatalError("Enum value BRAILLE was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value BRAILLE was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .BUGINESE: if let BUGINESE = classObj.BUGINESE { self.init(javaHolder: BUGINESE.javaHolder) } else { - fatalError("Enum value BUGINESE was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value BUGINESE was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .COPTIC: if let COPTIC = classObj.COPTIC { self.init(javaHolder: COPTIC.javaHolder) } else { - fatalError("Enum value COPTIC was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value COPTIC was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .NEW_TAI_LUE: if let NEW_TAI_LUE = classObj.NEW_TAI_LUE { self.init(javaHolder: NEW_TAI_LUE.javaHolder) } else { - fatalError("Enum value NEW_TAI_LUE was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value NEW_TAI_LUE was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .GLAGOLITIC: if let GLAGOLITIC = classObj.GLAGOLITIC { self.init(javaHolder: GLAGOLITIC.javaHolder) } else { - fatalError("Enum value GLAGOLITIC was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value GLAGOLITIC was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .TIFINAGH: if let TIFINAGH = classObj.TIFINAGH { self.init(javaHolder: TIFINAGH.javaHolder) } else { - fatalError("Enum value TIFINAGH was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value TIFINAGH was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .SYLOTI_NAGRI: if let SYLOTI_NAGRI = classObj.SYLOTI_NAGRI { self.init(javaHolder: SYLOTI_NAGRI.javaHolder) } else { - fatalError("Enum value SYLOTI_NAGRI was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value SYLOTI_NAGRI was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .OLD_PERSIAN: if let OLD_PERSIAN = classObj.OLD_PERSIAN { self.init(javaHolder: OLD_PERSIAN.javaHolder) } else { - fatalError("Enum value OLD_PERSIAN was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value OLD_PERSIAN was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .KHAROSHTHI: if let KHAROSHTHI = classObj.KHAROSHTHI { self.init(javaHolder: KHAROSHTHI.javaHolder) } else { - fatalError("Enum value KHAROSHTHI was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value KHAROSHTHI was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .BALINESE: if let BALINESE = classObj.BALINESE { self.init(javaHolder: BALINESE.javaHolder) } else { - fatalError("Enum value BALINESE was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value BALINESE was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .CUNEIFORM: if let CUNEIFORM = classObj.CUNEIFORM { self.init(javaHolder: CUNEIFORM.javaHolder) } else { - fatalError("Enum value CUNEIFORM was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value CUNEIFORM was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .PHOENICIAN: if let PHOENICIAN = classObj.PHOENICIAN { self.init(javaHolder: PHOENICIAN.javaHolder) } else { - fatalError("Enum value PHOENICIAN was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value PHOENICIAN was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .PHAGS_PA: if let PHAGS_PA = classObj.PHAGS_PA { self.init(javaHolder: PHAGS_PA.javaHolder) } else { - fatalError("Enum value PHAGS_PA was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value PHAGS_PA was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .NKO: if let NKO = classObj.NKO { self.init(javaHolder: NKO.javaHolder) } else { - fatalError("Enum value NKO was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value NKO was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .SUNDANESE: if let SUNDANESE = classObj.SUNDANESE { self.init(javaHolder: SUNDANESE.javaHolder) } else { - fatalError("Enum value SUNDANESE was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value SUNDANESE was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .BATAK: if let BATAK = classObj.BATAK { self.init(javaHolder: BATAK.javaHolder) } else { - fatalError("Enum value BATAK was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value BATAK was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .LEPCHA: if let LEPCHA = classObj.LEPCHA { self.init(javaHolder: LEPCHA.javaHolder) } else { - fatalError("Enum value LEPCHA was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value LEPCHA was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .OL_CHIKI: if let OL_CHIKI = classObj.OL_CHIKI { self.init(javaHolder: OL_CHIKI.javaHolder) } else { - fatalError("Enum value OL_CHIKI was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value OL_CHIKI was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .VAI: if let VAI = classObj.VAI { self.init(javaHolder: VAI.javaHolder) } else { - fatalError("Enum value VAI was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value VAI was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .SAURASHTRA: if let SAURASHTRA = classObj.SAURASHTRA { self.init(javaHolder: SAURASHTRA.javaHolder) } else { - fatalError("Enum value SAURASHTRA was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value SAURASHTRA was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .KAYAH_LI: if let KAYAH_LI = classObj.KAYAH_LI { self.init(javaHolder: KAYAH_LI.javaHolder) } else { - fatalError("Enum value KAYAH_LI was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value KAYAH_LI was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .REJANG: if let REJANG = classObj.REJANG { self.init(javaHolder: REJANG.javaHolder) } else { - fatalError("Enum value REJANG was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value REJANG was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .LYCIAN: if let LYCIAN = classObj.LYCIAN { self.init(javaHolder: LYCIAN.javaHolder) } else { - fatalError("Enum value LYCIAN was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value LYCIAN was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .CARIAN: if let CARIAN = classObj.CARIAN { self.init(javaHolder: CARIAN.javaHolder) } else { - fatalError("Enum value CARIAN was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value CARIAN was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .LYDIAN: if let LYDIAN = classObj.LYDIAN { self.init(javaHolder: LYDIAN.javaHolder) } else { - fatalError("Enum value LYDIAN was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value LYDIAN was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .CHAM: if let CHAM = classObj.CHAM { self.init(javaHolder: CHAM.javaHolder) } else { - fatalError("Enum value CHAM was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value CHAM was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .TAI_THAM: if let TAI_THAM = classObj.TAI_THAM { self.init(javaHolder: TAI_THAM.javaHolder) } else { - fatalError("Enum value TAI_THAM was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value TAI_THAM was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .TAI_VIET: if let TAI_VIET = classObj.TAI_VIET { self.init(javaHolder: TAI_VIET.javaHolder) } else { - fatalError("Enum value TAI_VIET was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value TAI_VIET was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .AVESTAN: if let AVESTAN = classObj.AVESTAN { self.init(javaHolder: AVESTAN.javaHolder) } else { - fatalError("Enum value AVESTAN was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value AVESTAN was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .EGYPTIAN_HIEROGLYPHS: if let EGYPTIAN_HIEROGLYPHS = classObj.EGYPTIAN_HIEROGLYPHS { self.init(javaHolder: EGYPTIAN_HIEROGLYPHS.javaHolder) } else { - fatalError("Enum value EGYPTIAN_HIEROGLYPHS was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value EGYPTIAN_HIEROGLYPHS was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .SAMARITAN: if let SAMARITAN = classObj.SAMARITAN { self.init(javaHolder: SAMARITAN.javaHolder) } else { - fatalError("Enum value SAMARITAN was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value SAMARITAN was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .MANDAIC: if let MANDAIC = classObj.MANDAIC { self.init(javaHolder: MANDAIC.javaHolder) } else { - fatalError("Enum value MANDAIC was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value MANDAIC was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .LISU: if let LISU = classObj.LISU { self.init(javaHolder: LISU.javaHolder) } else { - fatalError("Enum value LISU was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value LISU was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .BAMUM: if let BAMUM = classObj.BAMUM { self.init(javaHolder: BAMUM.javaHolder) } else { - fatalError("Enum value BAMUM was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value BAMUM was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .JAVANESE: if let JAVANESE = classObj.JAVANESE { self.init(javaHolder: JAVANESE.javaHolder) } else { - fatalError("Enum value JAVANESE was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value JAVANESE was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .MEETEI_MAYEK: if let MEETEI_MAYEK = classObj.MEETEI_MAYEK { self.init(javaHolder: MEETEI_MAYEK.javaHolder) } else { - fatalError("Enum value MEETEI_MAYEK was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value MEETEI_MAYEK was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .IMPERIAL_ARAMAIC: if let IMPERIAL_ARAMAIC = classObj.IMPERIAL_ARAMAIC { self.init(javaHolder: IMPERIAL_ARAMAIC.javaHolder) } else { - fatalError("Enum value IMPERIAL_ARAMAIC was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value IMPERIAL_ARAMAIC was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .OLD_SOUTH_ARABIAN: if let OLD_SOUTH_ARABIAN = classObj.OLD_SOUTH_ARABIAN { self.init(javaHolder: OLD_SOUTH_ARABIAN.javaHolder) } else { - fatalError("Enum value OLD_SOUTH_ARABIAN was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value OLD_SOUTH_ARABIAN was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .INSCRIPTIONAL_PARTHIAN: if let INSCRIPTIONAL_PARTHIAN = classObj.INSCRIPTIONAL_PARTHIAN { self.init(javaHolder: INSCRIPTIONAL_PARTHIAN.javaHolder) } else { - fatalError("Enum value INSCRIPTIONAL_PARTHIAN was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value INSCRIPTIONAL_PARTHIAN was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .INSCRIPTIONAL_PAHLAVI: if let INSCRIPTIONAL_PAHLAVI = classObj.INSCRIPTIONAL_PAHLAVI { self.init(javaHolder: INSCRIPTIONAL_PAHLAVI.javaHolder) } else { - fatalError("Enum value INSCRIPTIONAL_PAHLAVI was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value INSCRIPTIONAL_PAHLAVI was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .OLD_TURKIC: if let OLD_TURKIC = classObj.OLD_TURKIC { self.init(javaHolder: OLD_TURKIC.javaHolder) } else { - fatalError("Enum value OLD_TURKIC was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value OLD_TURKIC was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .BRAHMI: if let BRAHMI = classObj.BRAHMI { self.init(javaHolder: BRAHMI.javaHolder) } else { - fatalError("Enum value BRAHMI was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value BRAHMI was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .KAITHI: if let KAITHI = classObj.KAITHI { self.init(javaHolder: KAITHI.javaHolder) } else { - fatalError("Enum value KAITHI was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value KAITHI was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .MEROITIC_HIEROGLYPHS: if let MEROITIC_HIEROGLYPHS = classObj.MEROITIC_HIEROGLYPHS { self.init(javaHolder: MEROITIC_HIEROGLYPHS.javaHolder) } else { - fatalError("Enum value MEROITIC_HIEROGLYPHS was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value MEROITIC_HIEROGLYPHS was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .MEROITIC_CURSIVE: if let MEROITIC_CURSIVE = classObj.MEROITIC_CURSIVE { self.init(javaHolder: MEROITIC_CURSIVE.javaHolder) } else { - fatalError("Enum value MEROITIC_CURSIVE was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value MEROITIC_CURSIVE was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .SORA_SOMPENG: if let SORA_SOMPENG = classObj.SORA_SOMPENG { self.init(javaHolder: SORA_SOMPENG.javaHolder) } else { - fatalError("Enum value SORA_SOMPENG was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value SORA_SOMPENG was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .CHAKMA: if let CHAKMA = classObj.CHAKMA { self.init(javaHolder: CHAKMA.javaHolder) } else { - fatalError("Enum value CHAKMA was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value CHAKMA was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .SHARADA: if let SHARADA = classObj.SHARADA { self.init(javaHolder: SHARADA.javaHolder) } else { - fatalError("Enum value SHARADA was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value SHARADA was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .TAKRI: if let TAKRI = classObj.TAKRI { self.init(javaHolder: TAKRI.javaHolder) } else { - fatalError("Enum value TAKRI was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value TAKRI was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .MIAO: if let MIAO = classObj.MIAO { self.init(javaHolder: MIAO.javaHolder) } else { - fatalError("Enum value MIAO was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value MIAO was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .CAUCASIAN_ALBANIAN: if let CAUCASIAN_ALBANIAN = classObj.CAUCASIAN_ALBANIAN { self.init(javaHolder: CAUCASIAN_ALBANIAN.javaHolder) } else { - fatalError("Enum value CAUCASIAN_ALBANIAN was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value CAUCASIAN_ALBANIAN was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .BASSA_VAH: if let BASSA_VAH = classObj.BASSA_VAH { self.init(javaHolder: BASSA_VAH.javaHolder) } else { - fatalError("Enum value BASSA_VAH was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value BASSA_VAH was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .DUPLOYAN: if let DUPLOYAN = classObj.DUPLOYAN { self.init(javaHolder: DUPLOYAN.javaHolder) } else { - fatalError("Enum value DUPLOYAN was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value DUPLOYAN was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .ELBASAN: if let ELBASAN = classObj.ELBASAN { self.init(javaHolder: ELBASAN.javaHolder) } else { - fatalError("Enum value ELBASAN was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value ELBASAN was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .GRANTHA: if let GRANTHA = classObj.GRANTHA { self.init(javaHolder: GRANTHA.javaHolder) } else { - fatalError("Enum value GRANTHA was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value GRANTHA was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .PAHAWH_HMONG: if let PAHAWH_HMONG = classObj.PAHAWH_HMONG { self.init(javaHolder: PAHAWH_HMONG.javaHolder) } else { - fatalError("Enum value PAHAWH_HMONG was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value PAHAWH_HMONG was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .KHOJKI: if let KHOJKI = classObj.KHOJKI { self.init(javaHolder: KHOJKI.javaHolder) } else { - fatalError("Enum value KHOJKI was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value KHOJKI was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .LINEAR_A: if let LINEAR_A = classObj.LINEAR_A { self.init(javaHolder: LINEAR_A.javaHolder) } else { - fatalError("Enum value LINEAR_A was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value LINEAR_A was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .MAHAJANI: if let MAHAJANI = classObj.MAHAJANI { self.init(javaHolder: MAHAJANI.javaHolder) } else { - fatalError("Enum value MAHAJANI was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value MAHAJANI was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .MANICHAEAN: if let MANICHAEAN = classObj.MANICHAEAN { self.init(javaHolder: MANICHAEAN.javaHolder) } else { - fatalError("Enum value MANICHAEAN was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value MANICHAEAN was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .MENDE_KIKAKUI: if let MENDE_KIKAKUI = classObj.MENDE_KIKAKUI { self.init(javaHolder: MENDE_KIKAKUI.javaHolder) } else { - fatalError("Enum value MENDE_KIKAKUI was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value MENDE_KIKAKUI was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .MODI: if let MODI = classObj.MODI { self.init(javaHolder: MODI.javaHolder) } else { - fatalError("Enum value MODI was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value MODI was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .MRO: if let MRO = classObj.MRO { self.init(javaHolder: MRO.javaHolder) } else { - fatalError("Enum value MRO was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value MRO was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .OLD_NORTH_ARABIAN: if let OLD_NORTH_ARABIAN = classObj.OLD_NORTH_ARABIAN { self.init(javaHolder: OLD_NORTH_ARABIAN.javaHolder) } else { - fatalError("Enum value OLD_NORTH_ARABIAN was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value OLD_NORTH_ARABIAN was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .NABATAEAN: if let NABATAEAN = classObj.NABATAEAN { self.init(javaHolder: NABATAEAN.javaHolder) } else { - fatalError("Enum value NABATAEAN was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value NABATAEAN was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .PALMYRENE: if let PALMYRENE = classObj.PALMYRENE { self.init(javaHolder: PALMYRENE.javaHolder) } else { - fatalError("Enum value PALMYRENE was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value PALMYRENE was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .PAU_CIN_HAU: if let PAU_CIN_HAU = classObj.PAU_CIN_HAU { self.init(javaHolder: PAU_CIN_HAU.javaHolder) } else { - fatalError("Enum value PAU_CIN_HAU was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value PAU_CIN_HAU was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .OLD_PERMIC: if let OLD_PERMIC = classObj.OLD_PERMIC { self.init(javaHolder: OLD_PERMIC.javaHolder) } else { - fatalError("Enum value OLD_PERMIC was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value OLD_PERMIC was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .PSALTER_PAHLAVI: if let PSALTER_PAHLAVI = classObj.PSALTER_PAHLAVI { self.init(javaHolder: PSALTER_PAHLAVI.javaHolder) } else { - fatalError("Enum value PSALTER_PAHLAVI was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value PSALTER_PAHLAVI was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .SIDDHAM: if let SIDDHAM = classObj.SIDDHAM { self.init(javaHolder: SIDDHAM.javaHolder) } else { - fatalError("Enum value SIDDHAM was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value SIDDHAM was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .KHUDAWADI: if let KHUDAWADI = classObj.KHUDAWADI { self.init(javaHolder: KHUDAWADI.javaHolder) } else { - fatalError("Enum value KHUDAWADI was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value KHUDAWADI was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .TIRHUTA: if let TIRHUTA = classObj.TIRHUTA { self.init(javaHolder: TIRHUTA.javaHolder) } else { - fatalError("Enum value TIRHUTA was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value TIRHUTA was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .WARANG_CITI: if let WARANG_CITI = classObj.WARANG_CITI { self.init(javaHolder: WARANG_CITI.javaHolder) } else { - fatalError("Enum value WARANG_CITI was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value WARANG_CITI was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .AHOM: if let AHOM = classObj.AHOM { self.init(javaHolder: AHOM.javaHolder) } else { - fatalError("Enum value AHOM was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value AHOM was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .ANATOLIAN_HIEROGLYPHS: if let ANATOLIAN_HIEROGLYPHS = classObj.ANATOLIAN_HIEROGLYPHS { self.init(javaHolder: ANATOLIAN_HIEROGLYPHS.javaHolder) } else { - fatalError("Enum value ANATOLIAN_HIEROGLYPHS was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value ANATOLIAN_HIEROGLYPHS was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .HATRAN: if let HATRAN = classObj.HATRAN { self.init(javaHolder: HATRAN.javaHolder) } else { - fatalError("Enum value HATRAN was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value HATRAN was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .MULTANI: if let MULTANI = classObj.MULTANI { self.init(javaHolder: MULTANI.javaHolder) } else { - fatalError("Enum value MULTANI was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value MULTANI was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .OLD_HUNGARIAN: if let OLD_HUNGARIAN = classObj.OLD_HUNGARIAN { self.init(javaHolder: OLD_HUNGARIAN.javaHolder) } else { - fatalError("Enum value OLD_HUNGARIAN was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value OLD_HUNGARIAN was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .SIGNWRITING: if let SIGNWRITING = classObj.SIGNWRITING { self.init(javaHolder: SIGNWRITING.javaHolder) } else { - fatalError("Enum value SIGNWRITING was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value SIGNWRITING was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .ADLAM: if let ADLAM = classObj.ADLAM { self.init(javaHolder: ADLAM.javaHolder) } else { - fatalError("Enum value ADLAM was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value ADLAM was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .BHAIKSUKI: if let BHAIKSUKI = classObj.BHAIKSUKI { self.init(javaHolder: BHAIKSUKI.javaHolder) } else { - fatalError("Enum value BHAIKSUKI was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value BHAIKSUKI was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .MARCHEN: if let MARCHEN = classObj.MARCHEN { self.init(javaHolder: MARCHEN.javaHolder) } else { - fatalError("Enum value MARCHEN was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value MARCHEN was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .NEWA: if let NEWA = classObj.NEWA { self.init(javaHolder: NEWA.javaHolder) } else { - fatalError("Enum value NEWA was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value NEWA was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .OSAGE: if let OSAGE = classObj.OSAGE { self.init(javaHolder: OSAGE.javaHolder) } else { - fatalError("Enum value OSAGE was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value OSAGE was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .TANGUT: if let TANGUT = classObj.TANGUT { self.init(javaHolder: TANGUT.javaHolder) } else { - fatalError("Enum value TANGUT was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value TANGUT was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .MASARAM_GONDI: if let MASARAM_GONDI = classObj.MASARAM_GONDI { self.init(javaHolder: MASARAM_GONDI.javaHolder) } else { - fatalError("Enum value MASARAM_GONDI was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value MASARAM_GONDI was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .NUSHU: if let NUSHU = classObj.NUSHU { self.init(javaHolder: NUSHU.javaHolder) } else { - fatalError("Enum value NUSHU was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value NUSHU was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .SOYOMBO: if let SOYOMBO = classObj.SOYOMBO { self.init(javaHolder: SOYOMBO.javaHolder) } else { - fatalError("Enum value SOYOMBO was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value SOYOMBO was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .ZANABAZAR_SQUARE: if let ZANABAZAR_SQUARE = classObj.ZANABAZAR_SQUARE { self.init(javaHolder: ZANABAZAR_SQUARE.javaHolder) } else { - fatalError("Enum value ZANABAZAR_SQUARE was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value ZANABAZAR_SQUARE was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .HANIFI_ROHINGYA: if let HANIFI_ROHINGYA = classObj.HANIFI_ROHINGYA { self.init(javaHolder: HANIFI_ROHINGYA.javaHolder) } else { - fatalError("Enum value HANIFI_ROHINGYA was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value HANIFI_ROHINGYA was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .OLD_SOGDIAN: if let OLD_SOGDIAN = classObj.OLD_SOGDIAN { self.init(javaHolder: OLD_SOGDIAN.javaHolder) } else { - fatalError("Enum value OLD_SOGDIAN was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value OLD_SOGDIAN was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .SOGDIAN: if let SOGDIAN = classObj.SOGDIAN { self.init(javaHolder: SOGDIAN.javaHolder) } else { - fatalError("Enum value SOGDIAN was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value SOGDIAN was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .DOGRA: if let DOGRA = classObj.DOGRA { self.init(javaHolder: DOGRA.javaHolder) } else { - fatalError("Enum value DOGRA was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value DOGRA was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .GUNJALA_GONDI: if let GUNJALA_GONDI = classObj.GUNJALA_GONDI { self.init(javaHolder: GUNJALA_GONDI.javaHolder) } else { - fatalError("Enum value GUNJALA_GONDI was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value GUNJALA_GONDI was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .MAKASAR: if let MAKASAR = classObj.MAKASAR { self.init(javaHolder: MAKASAR.javaHolder) } else { - fatalError("Enum value MAKASAR was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value MAKASAR was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .MEDEFAIDRIN: if let MEDEFAIDRIN = classObj.MEDEFAIDRIN { self.init(javaHolder: MEDEFAIDRIN.javaHolder) } else { - fatalError("Enum value MEDEFAIDRIN was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value MEDEFAIDRIN was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .ELYMAIC: if let ELYMAIC = classObj.ELYMAIC { self.init(javaHolder: ELYMAIC.javaHolder) } else { - fatalError("Enum value ELYMAIC was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value ELYMAIC was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .NANDINAGARI: if let NANDINAGARI = classObj.NANDINAGARI { self.init(javaHolder: NANDINAGARI.javaHolder) } else { - fatalError("Enum value NANDINAGARI was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value NANDINAGARI was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .NYIAKENG_PUACHUE_HMONG: if let NYIAKENG_PUACHUE_HMONG = classObj.NYIAKENG_PUACHUE_HMONG { self.init(javaHolder: NYIAKENG_PUACHUE_HMONG.javaHolder) } else { - fatalError("Enum value NYIAKENG_PUACHUE_HMONG was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value NYIAKENG_PUACHUE_HMONG was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .WANCHO: if let WANCHO = classObj.WANCHO { self.init(javaHolder: WANCHO.javaHolder) } else { - fatalError("Enum value WANCHO was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value WANCHO was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .YEZIDI: if let YEZIDI = classObj.YEZIDI { self.init(javaHolder: YEZIDI.javaHolder) } else { - fatalError("Enum value YEZIDI was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value YEZIDI was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .CHORASMIAN: if let CHORASMIAN = classObj.CHORASMIAN { self.init(javaHolder: CHORASMIAN.javaHolder) } else { - fatalError("Enum value CHORASMIAN was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value CHORASMIAN was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .DIVES_AKURU: if let DIVES_AKURU = classObj.DIVES_AKURU { self.init(javaHolder: DIVES_AKURU.javaHolder) } else { - fatalError("Enum value DIVES_AKURU was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value DIVES_AKURU was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .KHITAN_SMALL_SCRIPT: if let KHITAN_SMALL_SCRIPT = classObj.KHITAN_SMALL_SCRIPT { self.init(javaHolder: KHITAN_SMALL_SCRIPT.javaHolder) } else { - fatalError("Enum value KHITAN_SMALL_SCRIPT was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value KHITAN_SMALL_SCRIPT was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .VITHKUQI: if let VITHKUQI = classObj.VITHKUQI { self.init(javaHolder: VITHKUQI.javaHolder) } else { - fatalError("Enum value VITHKUQI was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value VITHKUQI was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .OLD_UYGHUR: if let OLD_UYGHUR = classObj.OLD_UYGHUR { self.init(javaHolder: OLD_UYGHUR.javaHolder) } else { - fatalError("Enum value OLD_UYGHUR was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value OLD_UYGHUR was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .CYPRO_MINOAN: if let CYPRO_MINOAN = classObj.CYPRO_MINOAN { self.init(javaHolder: CYPRO_MINOAN.javaHolder) } else { - fatalError("Enum value CYPRO_MINOAN was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value CYPRO_MINOAN was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .TANGSA: if let TANGSA = classObj.TANGSA { self.init(javaHolder: TANGSA.javaHolder) } else { - fatalError("Enum value TANGSA was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value TANGSA was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .TOTO: if let TOTO = classObj.TOTO { self.init(javaHolder: TOTO.javaHolder) } else { - fatalError("Enum value TOTO was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value TOTO was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .KAWI: if let KAWI = classObj.KAWI { self.init(javaHolder: KAWI.javaHolder) } else { - fatalError("Enum value KAWI was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value KAWI was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .NAG_MUNDARI: if let NAG_MUNDARI = classObj.NAG_MUNDARI { self.init(javaHolder: NAG_MUNDARI.javaHolder) } else { - fatalError("Enum value NAG_MUNDARI was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value NAG_MUNDARI was unexpectedly nil, please re-run swift-java on the most updated Java class") } case .UNKNOWN: if let UNKNOWN = classObj.UNKNOWN { self.init(javaHolder: UNKNOWN.javaHolder) } else { - fatalError("Enum value UNKNOWN was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value UNKNOWN was unexpectedly nil, please re-run swift-java on the most updated Java class") } } } diff --git a/Sources/JavaKit/generated/JavaClass.swift b/Sources/SwiftJava/generated/JavaClass.swift similarity index 87% rename from Sources/JavaKit/generated/JavaClass.swift rename to Sources/SwiftJava/generated/JavaClass.swift index 03d7f5e6d..a1147e3c2 100644 --- a/Sources/JavaKit/generated/JavaClass.swift +++ b/Sources/SwiftJava/generated/JavaClass.swift @@ -1,7 +1,7 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaRuntime +import CSwiftJavaJNI -@JavaClass("java.lang.Class") +@JavaClass("java.lang.Class", implements: JavaReflectType.self) open class JavaClass: JavaObject { @JavaMethod open func getName() -> String @@ -52,13 +52,16 @@ open class JavaClass: JavaObject { open func isRecord() -> Bool @JavaMethod - open func getClassLoader() -> JavaClassLoader! + open func isSealed() -> Bool @JavaMethod - open func newInstance() throws -> JavaObject! + open func getInterfaces() -> [JavaClass?] @JavaMethod - open func getInterfaces() -> [JavaClass?] + open func getClassLoader() -> JavaClassLoader! + + @JavaMethod + open func newInstance() throws -> T! @JavaMethod open func isMemberClass() -> Bool @@ -70,7 +73,7 @@ open class JavaClass: JavaObject { open func isAnonymousClass() -> Bool @JavaMethod - open func getEnclosingClass() throws -> JavaClass! + open func getEnclosingClass() -> JavaClass! @JavaMethod open func arrayType() -> JavaClass! @@ -81,6 +84,9 @@ open class JavaClass: JavaObject { @JavaMethod open func getCanonicalName() -> String + @JavaMethod + open func getDeclaredClasses() -> [JavaClass?] + @JavaMethod open func getPackageName() -> String @@ -102,11 +108,17 @@ open class JavaClass: JavaObject { @JavaMethod open func isSynthetic() -> Bool + @JavaMethod + open func getGenericSuperclass() -> JavaReflectType! + + @JavaMethod + open func getGenericInterfaces() -> [JavaReflectType?] + @JavaMethod open func getSigners() -> [JavaObject?] @JavaMethod - open func getDeclaringClass() throws -> JavaClass! + open func getDeclaringClass() -> JavaClass! @JavaMethod open func getTypeName() -> String @@ -114,9 +126,6 @@ open class JavaClass: JavaObject { @JavaMethod open func getClasses() -> [JavaClass?] - @JavaMethod - open func getDeclaredClasses() throws -> [JavaClass?] - @JavaMethod open func getEnumConstants() -> [JavaObject?] @@ -128,16 +137,13 @@ open class JavaClass: JavaObject { @JavaMethod open func getNestMembers() -> [JavaClass?] - - @JavaMethod - open func isSealed() -> Bool } extension JavaClass { @JavaStaticMethod - public func forName(_ arg0: String, _ arg1: Bool, _ arg2: JavaClassLoader?) throws -> JavaClass! where ObjectType == JavaClass + public func forName(_ arg0: String) throws -> JavaClass! where ObjectType == JavaClass @JavaStaticMethod - public func forName(_ arg0: String) throws -> JavaClass! where ObjectType == JavaClass + public func forName(_ arg0: String, _ arg1: Bool, _ arg2: JavaClassLoader?) throws -> JavaClass! where ObjectType == JavaClass @JavaStaticMethod public func forPrimitiveName(_ arg0: String) -> JavaClass! where ObjectType == JavaClass diff --git a/Sources/JavaKit/generated/JavaClassLoader.swift b/Sources/SwiftJava/generated/JavaClassLoader.swift similarity index 98% rename from Sources/JavaKit/generated/JavaClassLoader.swift rename to Sources/SwiftJava/generated/JavaClassLoader.swift index 6c877cc30..0cd64aa15 100644 --- a/Sources/JavaKit/generated/JavaClassLoader.swift +++ b/Sources/SwiftJava/generated/JavaClassLoader.swift @@ -1,5 +1,5 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaRuntime +import CSwiftJavaJNI @JavaClass("java.lang.ClassLoader") open class JavaClassLoader: JavaObject { @@ -7,10 +7,10 @@ open class JavaClassLoader: JavaObject { open func getName() -> String @JavaMethod - open func loadClass(_ arg0: String, _ arg1: Bool) throws -> JavaClass! + open func loadClass(_ arg0: String) throws -> JavaClass! @JavaMethod - open func loadClass(_ arg0: String) throws -> JavaClass! + open func loadClass(_ arg0: String, _ arg1: Bool) throws -> JavaClass! @JavaMethod open func setSigners(_ arg0: JavaClass?, _ arg1: [JavaObject?]) @@ -22,10 +22,10 @@ open class JavaClassLoader: JavaObject { open func findLoadedClass(_ arg0: String) -> JavaClass! @JavaMethod - open func findClass(_ arg0: String) throws -> JavaClass! + open func findClass(_ arg0: String, _ arg1: String) -> JavaClass! @JavaMethod - open func findClass(_ arg0: String, _ arg1: String) -> JavaClass! + open func findClass(_ arg0: String) throws -> JavaClass! @JavaMethod open func resolveClass(_ arg0: JavaClass?) diff --git a/Sources/JavaKit/generated/JavaDouble.swift b/Sources/SwiftJava/generated/JavaDouble.swift similarity index 99% rename from Sources/JavaKit/generated/JavaDouble.swift rename to Sources/SwiftJava/generated/JavaDouble.swift index efa77a955..8d54f8de6 100644 --- a/Sources/JavaKit/generated/JavaDouble.swift +++ b/Sources/SwiftJava/generated/JavaDouble.swift @@ -1,5 +1,5 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaRuntime +import CSwiftJavaJNI @JavaClass("java.lang.Double") open class JavaDouble: JavaNumber { diff --git a/Sources/SwiftJava/generated/JavaEnum.swift b/Sources/SwiftJava/generated/JavaEnum.swift new file mode 100644 index 000000000..2b8e102c9 --- /dev/null +++ b/Sources/SwiftJava/generated/JavaEnum.swift @@ -0,0 +1,11 @@ +// // Auto-generated by Java-to-Swift wrapper generator. +// import CSwiftJavaJNI + +// @JavaClass("java.lang.Enum") +// open class JavaEnum: JavaObject { +// @JavaMethod +// public func name() -> String + +// @JavaMethod +// public func ordinal() -> Int32 +// } diff --git a/Sources/JavaKit/generated/JavaError.swift b/Sources/SwiftJava/generated/JavaError.swift similarity index 96% rename from Sources/JavaKit/generated/JavaError.swift rename to Sources/SwiftJava/generated/JavaError.swift index 97c2e5559..4ba9d2ca1 100644 --- a/Sources/JavaKit/generated/JavaError.swift +++ b/Sources/SwiftJava/generated/JavaError.swift @@ -1,5 +1,5 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaRuntime +import CSwiftJavaJNI @JavaClass("java.lang.Error") open class JavaError: Throwable { diff --git a/Sources/JavaKit/generated/JavaFloat.swift b/Sources/SwiftJava/generated/JavaFloat.swift similarity index 99% rename from Sources/JavaKit/generated/JavaFloat.swift rename to Sources/SwiftJava/generated/JavaFloat.swift index 0c38d1aec..ac989531d 100644 --- a/Sources/JavaKit/generated/JavaFloat.swift +++ b/Sources/SwiftJava/generated/JavaFloat.swift @@ -1,5 +1,5 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaRuntime +import CSwiftJavaJNI @JavaClass("java.lang.Float") open class JavaFloat: JavaNumber { diff --git a/Sources/JavaKit/generated/JavaInteger.swift b/Sources/SwiftJava/generated/JavaInteger.swift similarity index 94% rename from Sources/JavaKit/generated/JavaInteger.swift rename to Sources/SwiftJava/generated/JavaInteger.swift index 646aac9ec..df57ba665 100644 --- a/Sources/JavaKit/generated/JavaInteger.swift +++ b/Sources/SwiftJava/generated/JavaInteger.swift @@ -1,9 +1,8 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaRuntime +import CSwiftJavaJNI @JavaClass("java.lang.Integer") open class JavaInteger: JavaNumber { - @JavaMethod @_nonoverride public convenience init(_ arg0: Int32, environment: JNIEnvironment? = nil) @@ -121,10 +120,10 @@ extension JavaClass { public func valueOf(_ arg0: String) throws -> JavaInteger! @JavaStaticMethod - public func valueOf(_ arg0: String, _ arg1: Int32) throws -> JavaInteger! + public func valueOf(_ arg0: Int32) -> JavaInteger! @JavaStaticMethod - public func valueOf(_ arg0: Int32) -> JavaInteger! + public func valueOf(_ arg0: String, _ arg1: Int32) throws -> JavaInteger! @JavaStaticMethod public func toHexString(_ arg0: Int32) -> String @@ -133,31 +132,37 @@ extension JavaClass { public func decode(_ arg0: String) throws -> JavaInteger! @JavaStaticMethod - public func parseInt(_ arg0: String) throws -> Int32 + public func parseInt(_ arg0: CharSequence?, _ arg1: Int32, _ arg2: Int32, _ arg3: Int32) throws -> Int32 @JavaStaticMethod public func parseInt(_ arg0: String, _ arg1: Int32) throws -> Int32 @JavaStaticMethod - public func toUnsignedLong(_ arg0: Int32) -> Int64 + public func parseInt(_ arg0: String) throws -> Int32 @JavaStaticMethod - public func sum(_ arg0: Int32, _ arg1: Int32) -> Int32 + public func highestOneBit(_ arg0: Int32) -> Int32 @JavaStaticMethod - public func toUnsignedString(_ arg0: Int32, _ arg1: Int32) -> String + public func toUnsignedLong(_ arg0: Int32) -> Int64 + + @JavaStaticMethod + public func sum(_ arg0: Int32, _ arg1: Int32) -> Int32 @JavaStaticMethod public func toUnsignedString(_ arg0: Int32) -> String @JavaStaticMethod - public func parseUnsignedInt(_ arg0: String) throws -> Int32 + public func toUnsignedString(_ arg0: Int32, _ arg1: Int32) -> String @JavaStaticMethod public func parseUnsignedInt(_ arg0: String, _ arg1: Int32) throws -> Int32 @JavaStaticMethod - public func getInteger(_ arg0: String, _ arg1: JavaInteger?) -> JavaInteger! + public func parseUnsignedInt(_ arg0: String) throws -> Int32 + + @JavaStaticMethod + public func parseUnsignedInt(_ arg0: CharSequence?, _ arg1: Int32, _ arg2: Int32, _ arg3: Int32) throws -> Int32 @JavaStaticMethod public func getInteger(_ arg0: String, _ arg1: Int32) -> JavaInteger! @@ -166,13 +171,13 @@ extension JavaClass { public func getInteger(_ arg0: String) -> JavaInteger! @JavaStaticMethod - public func toOctalString(_ arg0: Int32) -> String + public func getInteger(_ arg0: String, _ arg1: JavaInteger?) -> JavaInteger! @JavaStaticMethod - public func toBinaryString(_ arg0: Int32) -> String + public func toOctalString(_ arg0: Int32) -> String @JavaStaticMethod - public func highestOneBit(_ arg0: Int32) -> Int32 + public func toBinaryString(_ arg0: Int32) -> String @JavaStaticMethod public func lowestOneBit(_ arg0: Int32) -> Int32 diff --git a/Sources/JavaKit/generated/JavaLong.swift b/Sources/SwiftJava/generated/JavaLong.swift similarity index 99% rename from Sources/JavaKit/generated/JavaLong.swift rename to Sources/SwiftJava/generated/JavaLong.swift index 7ff70efa5..4e993d65e 100644 --- a/Sources/JavaKit/generated/JavaLong.swift +++ b/Sources/SwiftJava/generated/JavaLong.swift @@ -1,5 +1,5 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaRuntime +import CSwiftJavaJNI @JavaClass("java.lang.Long") open class JavaLong: JavaNumber { @@ -126,10 +126,10 @@ extension JavaClass { public func compare(_ arg0: Int64, _ arg1: Int64) -> Int32 @JavaStaticMethod - public func valueOf(_ arg0: String) throws -> JavaLong! + public func valueOf(_ arg0: Int64) -> JavaLong! @JavaStaticMethod - public func valueOf(_ arg0: Int64) -> JavaLong! + public func valueOf(_ arg0: String) throws -> JavaLong! @JavaStaticMethod public func valueOf(_ arg0: String, _ arg1: Int32) throws -> JavaLong! @@ -168,20 +168,20 @@ extension JavaClass { public func rotateRight(_ arg0: Int64, _ arg1: Int32) -> Int64 @JavaStaticMethod - public func parseLong(_ arg0: CharSequence?, _ arg1: Int32, _ arg2: Int32, _ arg3: Int32) throws -> Int64 + public func parseLong(_ arg0: String) throws -> Int64 @JavaStaticMethod public func parseLong(_ arg0: String, _ arg1: Int32) throws -> Int64 @JavaStaticMethod - public func parseLong(_ arg0: String) throws -> Int64 + public func parseLong(_ arg0: CharSequence?, _ arg1: Int32, _ arg2: Int32, _ arg3: Int32) throws -> Int64 @JavaStaticMethod - public func parseUnsignedLong(_ arg0: CharSequence?, _ arg1: Int32, _ arg2: Int32, _ arg3: Int32) throws -> Int64 + public func parseUnsignedLong(_ arg0: String) throws -> Int64 @JavaStaticMethod public func parseUnsignedLong(_ arg0: String, _ arg1: Int32) throws -> Int64 @JavaStaticMethod - public func parseUnsignedLong(_ arg0: String) throws -> Int64 + public func parseUnsignedLong(_ arg0: CharSequence?, _ arg1: Int32, _ arg2: Int32, _ arg3: Int32) throws -> Int64 } diff --git a/Sources/JavaKit/generated/JavaNumber.swift b/Sources/SwiftJava/generated/JavaNumber.swift similarity index 96% rename from Sources/JavaKit/generated/JavaNumber.swift rename to Sources/SwiftJava/generated/JavaNumber.swift index 414cd89b1..78f988f10 100644 --- a/Sources/JavaKit/generated/JavaNumber.swift +++ b/Sources/SwiftJava/generated/JavaNumber.swift @@ -1,5 +1,5 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaRuntime +import CSwiftJavaJNI @JavaClass("java.lang.Number") open class JavaNumber: JavaObject { diff --git a/Sources/JavaKit/generated/JavaObject.swift b/Sources/SwiftJava/generated/JavaObject.swift similarity index 97% rename from Sources/JavaKit/generated/JavaObject.swift rename to Sources/SwiftJava/generated/JavaObject.swift index 07a6eaff1..7db8a965c 100644 --- a/Sources/JavaKit/generated/JavaObject.swift +++ b/Sources/SwiftJava/generated/JavaObject.swift @@ -1,5 +1,5 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaRuntime +import CSwiftJavaJNI @JavaClass("java.lang.Object") open class JavaObject { diff --git a/Sources/JavaKit/generated/JavaOptional.swift b/Sources/SwiftJava/generated/JavaOptional.swift similarity index 94% rename from Sources/JavaKit/generated/JavaOptional.swift rename to Sources/SwiftJava/generated/JavaOptional.swift index bd77cfed2..f3b7adcab 100644 --- a/Sources/JavaKit/generated/JavaOptional.swift +++ b/Sources/SwiftJava/generated/JavaOptional.swift @@ -1,10 +1,10 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaRuntime +import CSwiftJavaJNI @JavaClass("java.util.Optional") open class JavaOptional: JavaObject { - @JavaMethod - open func get() -> JavaObject! + @JavaMethod(typeErasedResult: "T") + open func get() -> T! @JavaMethod open override func equals(_ arg0: JavaObject?) -> Bool diff --git a/Sources/JavaKit/generated/JavaOptionalDouble.swift b/Sources/SwiftJava/generated/JavaOptionalDouble.swift similarity index 97% rename from Sources/JavaKit/generated/JavaOptionalDouble.swift rename to Sources/SwiftJava/generated/JavaOptionalDouble.swift index 5926282ae..58aa94419 100644 --- a/Sources/JavaKit/generated/JavaOptionalDouble.swift +++ b/Sources/SwiftJava/generated/JavaOptionalDouble.swift @@ -1,5 +1,5 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaRuntime +import CSwiftJavaJNI @JavaClass("java.util.OptionalDouble") open class JavaOptionalDouble: JavaObject { @@ -16,10 +16,10 @@ open class JavaOptionalDouble: JavaObject { open func isEmpty() -> Bool @JavaMethod - open func isPresent() -> Bool + open func orElse(_ arg0: Double) -> Double @JavaMethod - open func orElse(_ arg0: Double) -> Double + open func isPresent() -> Bool @JavaMethod open func orElseThrow() -> Double diff --git a/Sources/JavaKit/generated/JavaOptionalInt.swift b/Sources/SwiftJava/generated/JavaOptionalInt.swift similarity index 97% rename from Sources/JavaKit/generated/JavaOptionalInt.swift rename to Sources/SwiftJava/generated/JavaOptionalInt.swift index 1237a085e..2270e66e4 100644 --- a/Sources/JavaKit/generated/JavaOptionalInt.swift +++ b/Sources/SwiftJava/generated/JavaOptionalInt.swift @@ -1,5 +1,5 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaRuntime +import CSwiftJavaJNI @JavaClass("java.util.OptionalInt") open class JavaOptionalInt: JavaObject { diff --git a/Sources/JavaKit/generated/JavaOptionalLong.swift b/Sources/SwiftJava/generated/JavaOptionalLong.swift similarity index 97% rename from Sources/JavaKit/generated/JavaOptionalLong.swift rename to Sources/SwiftJava/generated/JavaOptionalLong.swift index 79a9e06f7..10c3fbd0f 100644 --- a/Sources/JavaKit/generated/JavaOptionalLong.swift +++ b/Sources/SwiftJava/generated/JavaOptionalLong.swift @@ -1,5 +1,5 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaRuntime +import CSwiftJavaJNI @JavaClass("java.util.OptionalLong") open class JavaOptionalLong: JavaObject { diff --git a/Sources/SwiftJava/generated/JavaReflectArray.swift b/Sources/SwiftJava/generated/JavaReflectArray.swift new file mode 100644 index 000000000..4cae1202d --- /dev/null +++ b/Sources/SwiftJava/generated/JavaReflectArray.swift @@ -0,0 +1,71 @@ +// Auto-generated by Java-to-Swift wrapper generator. +import CSwiftJavaJNI + +@JavaClass("java.lang.reflect.Array") +open class JavaReflectArray: JavaObject { + +} +extension JavaClass { + @JavaStaticMethod + public func get(_ arg0: JavaObject?, _ arg1: Int32) throws -> JavaObject! + + @JavaStaticMethod + public func getLength(_ arg0: JavaObject?) throws -> Int32 + + @JavaStaticMethod + public func getBoolean(_ arg0: JavaObject?, _ arg1: Int32) throws -> Bool + + @JavaStaticMethod + public func getByte(_ arg0: JavaObject?, _ arg1: Int32) throws -> Int8 + + @JavaStaticMethod + public func getShort(_ arg0: JavaObject?, _ arg1: Int32) throws -> Int16 + + @JavaStaticMethod + public func getChar(_ arg0: JavaObject?, _ arg1: Int32) throws -> UInt16 + + @JavaStaticMethod + public func getInt(_ arg0: JavaObject?, _ arg1: Int32) throws -> Int32 + + @JavaStaticMethod + public func getLong(_ arg0: JavaObject?, _ arg1: Int32) throws -> Int64 + + @JavaStaticMethod + public func getFloat(_ arg0: JavaObject?, _ arg1: Int32) throws -> Float + + @JavaStaticMethod + public func getDouble(_ arg0: JavaObject?, _ arg1: Int32) throws -> Double + + @JavaStaticMethod + public func newInstance(_ arg0: JavaClass?, _ arg1: Int32) throws -> JavaObject! + + @JavaStaticMethod + public func newInstance(_ arg0: JavaClass?, _ arg1: [Int32]) throws -> JavaObject! + + @JavaStaticMethod + public func set(_ arg0: JavaObject?, _ arg1: Int32, _ arg2: JavaObject?) throws + + @JavaStaticMethod + public func setBoolean(_ arg0: JavaObject?, _ arg1: Int32, _ arg2: Bool) throws + + @JavaStaticMethod + public func setByte(_ arg0: JavaObject?, _ arg1: Int32, _ arg2: Int8) throws + + @JavaStaticMethod + public func setChar(_ arg0: JavaObject?, _ arg1: Int32, _ arg2: UInt16) throws + + @JavaStaticMethod + public func setShort(_ arg0: JavaObject?, _ arg1: Int32, _ arg2: Int16) throws + + @JavaStaticMethod + public func setInt(_ arg0: JavaObject?, _ arg1: Int32, _ arg2: Int32) throws + + @JavaStaticMethod + public func setLong(_ arg0: JavaObject?, _ arg1: Int32, _ arg2: Int64) throws + + @JavaStaticMethod + public func setFloat(_ arg0: JavaObject?, _ arg1: Int32, _ arg2: Float) throws + + @JavaStaticMethod + public func setDouble(_ arg0: JavaObject?, _ arg1: Int32, _ arg2: Double) throws +} diff --git a/Sources/SwiftJava/generated/JavaReflectParameterizedType.swift b/Sources/SwiftJava/generated/JavaReflectParameterizedType.swift new file mode 100644 index 000000000..a08016445 --- /dev/null +++ b/Sources/SwiftJava/generated/JavaReflectParameterizedType.swift @@ -0,0 +1,17 @@ +// Auto-generated by Java-to-Swift wrapper generator. +import CSwiftJavaJNI + +@JavaInterface("java.lang.reflect.ParameterizedType", extends: JavaReflectType.self) +public struct JavaReflectParameterizedType { + @JavaMethod + public func getOwnerType() -> JavaReflectType! + + @JavaMethod + public func getRawType() -> JavaReflectType! + + @JavaMethod + public func getActualTypeArguments() -> [JavaReflectType?] + + @JavaMethod + public func getTypeName() -> String +} diff --git a/Sources/JavaKitReflection/generated/Type.swift b/Sources/SwiftJava/generated/JavaReflectType.swift similarity index 73% rename from Sources/JavaKitReflection/generated/Type.swift rename to Sources/SwiftJava/generated/JavaReflectType.swift index ea6376cb4..fdf3b5726 100644 --- a/Sources/JavaKitReflection/generated/Type.swift +++ b/Sources/SwiftJava/generated/JavaReflectType.swift @@ -1,9 +1,8 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaKit -import JavaRuntime +import CSwiftJavaJNI @JavaInterface("java.lang.reflect.Type") -public struct Type { +public struct JavaReflectType { @JavaMethod public func getTypeName() -> String } diff --git a/Sources/JavaKit/generated/JavaShort.swift b/Sources/SwiftJava/generated/JavaShort.swift similarity index 99% rename from Sources/JavaKit/generated/JavaShort.swift rename to Sources/SwiftJava/generated/JavaShort.swift index f425ae184..4f387b36c 100644 --- a/Sources/JavaKit/generated/JavaShort.swift +++ b/Sources/SwiftJava/generated/JavaShort.swift @@ -1,5 +1,5 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaRuntime +import CSwiftJavaJNI @JavaClass("java.lang.Short") open class JavaShort: JavaNumber { diff --git a/Sources/JavaKit/generated/JavaString.swift b/Sources/SwiftJava/generated/JavaString.swift similarity index 99% rename from Sources/JavaKit/generated/JavaString.swift rename to Sources/SwiftJava/generated/JavaString.swift index c5f627f21..f3372f657 100644 --- a/Sources/JavaKit/generated/JavaString.swift +++ b/Sources/SwiftJava/generated/JavaString.swift @@ -1,5 +1,5 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaRuntime +import CSwiftJavaJNI @JavaClass("java.lang.String", implements: CharSequence.self) open class JavaString: JavaObject { diff --git a/Sources/SwiftJava/generated/JavaThread.swift b/Sources/SwiftJava/generated/JavaThread.swift new file mode 100644 index 000000000..c71e933b4 --- /dev/null +++ b/Sources/SwiftJava/generated/JavaThread.swift @@ -0,0 +1,229 @@ +// Auto-generated by Java-to-Swift wrapper generator. +import CSwiftJavaJNI + +@JavaClass("java.lang.Thread") +open class JavaThread: JavaObject { + @JavaMethod + @_nonoverride public convenience init(_ arg0: String, environment: JNIEnvironment? = nil) + + @JavaMethod + @_nonoverride public convenience init(environment: JNIEnvironment? = nil) + + @JavaMethod + open func getName() -> String + + @JavaMethod + open func run() + + @JavaMethod + open func interrupt() + + @JavaMethod + open override func toString() -> String + + @JavaMethod + open override func clone() throws -> JavaObject! + + @JavaMethod + open func join(_ arg0: Int64, _ arg1: Int32) throws + + @JavaMethod + open func join() throws + + @JavaMethod + open func join(_ arg0: Int64) throws + + @JavaMethod + open func setContextClassLoader(_ arg0: JavaClassLoader?) + + @JavaMethod + open func setPriority(_ arg0: Int32) + + @JavaMethod + open func setDaemon(_ arg0: Bool) + + @JavaMethod + open func start() + + @JavaMethod + open func getPriority() -> Int32 + + @JavaMethod + open func isDaemon() -> Bool + + @JavaMethod + open func getContextClassLoader() -> JavaClassLoader! + + @JavaMethod + open func isVirtual() -> Bool + + @JavaMethod + open func isAlive() -> Bool + + @JavaMethod + open func threadId() -> Int64 + + @JavaMethod + open func getUncaughtExceptionHandler() -> JavaThread.UncaughtExceptionHandler! + + @JavaMethod + open func stop() + + @JavaMethod + open func isInterrupted() -> Bool + + @JavaMethod + open func setName(_ arg0: String) + + @JavaMethod + open func checkAccess() + + @JavaMethod + open func getId() -> Int64 + + @JavaMethod + open func setUncaughtExceptionHandler(_ arg0: JavaThread.UncaughtExceptionHandler?) +} +extension JavaThread { + @JavaInterface("java.lang.Thread$Builder") + public struct Builder { + @JavaMethod + public func name(_ arg0: String) -> JavaThread.Builder! + + @JavaMethod + public func name(_ arg0: String, _ arg1: Int64) -> JavaThread.Builder! + + @JavaMethod + public func uncaughtExceptionHandler(_ arg0: JavaThread.UncaughtExceptionHandler?) -> JavaThread.Builder! + + @JavaMethod + public func inheritInheritableThreadLocals(_ arg0: Bool) -> JavaThread.Builder! + } +} +extension JavaThread.Builder { + @JavaInterface("java.lang.Thread$Builder$OfPlatform", extends: JavaThread.Builder.self) + public struct OfPlatform { + @JavaMethod + public func name(_ arg0: String, _ arg1: Int64) -> JavaThread.Builder! + + @JavaMethod + public func name(_ arg0: String, _ arg1: Int64) -> JavaThread.Builder.OfPlatform! + + @JavaMethod + public func name(_ arg0: String) -> JavaThread.Builder! + + @JavaMethod + public func name(_ arg0: String) -> JavaThread.Builder.OfPlatform! + + @JavaMethod + public func priority(_ arg0: Int32) -> JavaThread.Builder.OfPlatform! + + @JavaMethod + public func daemon() -> JavaThread.Builder.OfPlatform! + + @JavaMethod + public func daemon(_ arg0: Bool) -> JavaThread.Builder.OfPlatform! + + @JavaMethod + public func uncaughtExceptionHandler(_ arg0: JavaThread.UncaughtExceptionHandler?) -> JavaThread.Builder.OfPlatform! + + @JavaMethod + public func uncaughtExceptionHandler(_ arg0: JavaThread.UncaughtExceptionHandler?) -> JavaThread.Builder! + + @JavaMethod + public func stackSize(_ arg0: Int64) -> JavaThread.Builder.OfPlatform! + + @JavaMethod + public func inheritInheritableThreadLocals(_ arg0: Bool) -> JavaThread.Builder.OfPlatform! + + @JavaMethod + public func inheritInheritableThreadLocals(_ arg0: Bool) -> JavaThread.Builder! + } +} +extension JavaThread.Builder { + @JavaInterface("java.lang.Thread$Builder$OfVirtual", extends: JavaThread.Builder.self) + public struct OfVirtual { + @JavaMethod + public func name(_ arg0: String, _ arg1: Int64) -> JavaThread.Builder! + + @JavaMethod + public func name(_ arg0: String, _ arg1: Int64) -> JavaThread.Builder.OfVirtual! + + @JavaMethod + public func name(_ arg0: String) -> JavaThread.Builder! + + @JavaMethod + public func name(_ arg0: String) -> JavaThread.Builder.OfVirtual! + + @JavaMethod + public func uncaughtExceptionHandler(_ arg0: JavaThread.UncaughtExceptionHandler?) -> JavaThread.Builder! + + @JavaMethod + public func uncaughtExceptionHandler(_ arg0: JavaThread.UncaughtExceptionHandler?) -> JavaThread.Builder.OfVirtual! + + @JavaMethod + public func inheritInheritableThreadLocals(_ arg0: Bool) -> JavaThread.Builder.OfVirtual! + + @JavaMethod + public func inheritInheritableThreadLocals(_ arg0: Bool) -> JavaThread.Builder! + } +} +extension JavaThread { + @JavaInterface("java.lang.Thread$UncaughtExceptionHandler") + public struct UncaughtExceptionHandler { + @JavaMethod + public func uncaughtException(_ arg0: JavaThread?, _ arg1: Throwable?) + } +} +extension JavaClass { + @JavaStaticField(isFinal: true) + public var MIN_PRIORITY: Int32 + + @JavaStaticField(isFinal: true) + public var NORM_PRIORITY: Int32 + + @JavaStaticField(isFinal: true) + public var MAX_PRIORITY: Int32 + + @JavaStaticMethod + public func currentThread() -> JavaThread! + + @JavaStaticMethod + public func onSpinWait() + + @JavaStaticMethod + public func holdsLock(_ arg0: JavaObject?) -> Bool + + @JavaStaticMethod + public func interrupted() -> Bool + + @JavaStaticMethod + public func activeCount() -> Int32 + + @JavaStaticMethod + public func enumerate(_ arg0: [JavaThread?]) -> Int32 + + @JavaStaticMethod + public func yield() + + @JavaStaticMethod + public func sleep(_ arg0: Int64) throws + + @JavaStaticMethod + public func sleep(_ arg0: Int64, _ arg1: Int32) throws + + @JavaStaticMethod + public func ofPlatform() -> JavaThread.Builder.OfPlatform! + + @JavaStaticMethod + public func ofVirtual() -> JavaThread.Builder.OfVirtual! + + @JavaStaticMethod + public func dumpStack() + + @JavaStaticMethod + public func setDefaultUncaughtExceptionHandler(_ arg0: JavaThread.UncaughtExceptionHandler?) + + @JavaStaticMethod + public func getDefaultUncaughtExceptionHandler() -> JavaThread.UncaughtExceptionHandler! +} diff --git a/Sources/JavaKit/generated/JavaVoid.swift b/Sources/SwiftJava/generated/JavaVoid.swift similarity index 91% rename from Sources/JavaKit/generated/JavaVoid.swift rename to Sources/SwiftJava/generated/JavaVoid.swift index 76a7334a0..54decbbc6 100644 --- a/Sources/JavaKit/generated/JavaVoid.swift +++ b/Sources/SwiftJava/generated/JavaVoid.swift @@ -1,5 +1,5 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaRuntime +import CSwiftJavaJNI @JavaClass("java.lang.Void") open class JavaVoid: JavaObject { diff --git a/Sources/JavaKit/generated/RuntimeException.swift b/Sources/SwiftJava/generated/RuntimeException.swift similarity index 96% rename from Sources/JavaKit/generated/RuntimeException.swift rename to Sources/SwiftJava/generated/RuntimeException.swift index c3e325062..14516ed11 100644 --- a/Sources/JavaKit/generated/RuntimeException.swift +++ b/Sources/SwiftJava/generated/RuntimeException.swift @@ -1,5 +1,5 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaRuntime +import CSwiftJavaJNI @JavaClass("java.lang.RuntimeException") open class RuntimeException: Exception { diff --git a/Sources/JavaKit/generated/Throwable.swift b/Sources/SwiftJava/generated/Throwable.swift similarity index 98% rename from Sources/JavaKit/generated/Throwable.swift rename to Sources/SwiftJava/generated/Throwable.swift index 574fedf56..7df74b7e8 100644 --- a/Sources/JavaKit/generated/Throwable.swift +++ b/Sources/SwiftJava/generated/Throwable.swift @@ -1,5 +1,5 @@ // Auto-generated by Java-to-Swift wrapper generator. -import JavaRuntime +import CSwiftJavaJNI @JavaClass("java.lang.Throwable") open class Throwable: JavaObject { diff --git a/Sources/JavaKit/swift-java.config b/Sources/SwiftJava/swift-java.config similarity index 86% rename from Sources/JavaKit/swift-java.config rename to Sources/SwiftJava/swift-java.config index b45671a7a..d43096a73 100644 --- a/Sources/JavaKit/swift-java.config +++ b/Sources/SwiftJava/swift-java.config @@ -5,6 +5,8 @@ "java.lang.Byte" : "JavaByte", "java.lang.Character" : "JavaCharacter", "java.lang.Class" : "JavaClass", + "java.lang.reflect.Type" : "JavaReflectType", + "java.lang.reflect.ParameterizedType" : "JavaReflectParameterizedType", "java.lang.ClassLoader" : "JavaClassLoader", "java.lang.Double" : "JavaDouble", "java.lang.Error" : "JavaError", @@ -21,6 +23,7 @@ "java.lang.Void" : "JavaVoid", "java.lang.CharSequence": "CharSequence", "java.lang.Appendable": "Appendable", + "java.lang.Thread": "JavaThread", "java.util.Optional": "JavaOptional", "java.util.OptionalDouble": "JavaOptionalDouble", "java.util.OptionalInt": "JavaOptionalInt", diff --git a/Sources/JavaKitConfigurationShared/Configuration.swift b/Sources/SwiftJavaConfigurationShared/Configuration.swift similarity index 62% rename from Sources/JavaKitConfigurationShared/Configuration.swift rename to Sources/SwiftJavaConfigurationShared/Configuration.swift index 2314a1b8e..e0c40f1c5 100644 --- a/Sources/JavaKitConfigurationShared/Configuration.swift +++ b/Sources/SwiftJavaConfigurationShared/Configuration.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Copyright (c) 2024-2025 Apple Inc. and the Swift.org project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -38,19 +38,47 @@ public struct Configuration: Codable { public var outputJavaDirectory: String? - public var mode: GenerationMode? + public var mode: JExtractGenerationMode? + public var effectiveMode: JExtractGenerationMode { + mode ?? .default + } + + public var writeEmptyFiles: Bool? // FIXME: default it to false, but that plays not nice with Codable + + public var unsignedNumbersMode: JExtractUnsignedIntegerMode? + public var effectiveUnsignedNumbersMode: JExtractUnsignedIntegerMode { + unsignedNumbersMode ?? .default + } + + public var minimumInputAccessLevelMode: JExtractMinimumAccessLevelMode? + public var effectiveMinimumInputAccessLevelMode: JExtractMinimumAccessLevelMode { + minimumInputAccessLevelMode ?? .default + } + + public var memoryManagementMode: JExtractMemoryManagementMode? + public var effectiveMemoryManagementMode: JExtractMemoryManagementMode { + memoryManagementMode ?? .default + } + + public var asyncFuncMode: JExtractAsyncFuncMode? + public var effectiveAsyncFuncMode: JExtractAsyncFuncMode { + asyncFuncMode ?? .default + } - // ==== java 2 swift --------------------------------------------------------- + public var enableJavaCallbacks: Bool? + public var effectiveEnableJavaCallbacks: Bool { + enableJavaCallbacks ?? false + } + + public var generatedJavaSourcesListFileOutput: String? + + // ==== wrap-java --------------------------------------------------------- - /// The Java class path that should be passed along to the Java2Swift tool. + /// The Java class path that should be passed along to the swift-java tool. public var classpath: String? = nil public var classpathEntries: [String] { - guard let classpath else { - return [] - } - - return classpath.split(separator: ":").map(String.init) + return classpath?.split(separator: ":").map(String.init) ?? [] } /// The Java classes that should be translated to Swift. The keys are @@ -64,6 +92,14 @@ public struct Configuration: Codable { // Generate class files suitable for the specified Java SE release. public var targetCompatibility: JavaVersion? + /// Filter input Java types by their package prefix if set. + public var filterInclude: [String]? + + /// Exclude input Java types by their package prefix or exact match. + public var filterExclude: [String]? + + public var singleSwiftFileOutput: String? + // ==== dependencies --------------------------------------------------------- // Java dependencies we need to fetch for this target. @@ -80,6 +116,12 @@ public struct JavaDependencyDescriptor: Hashable, Codable { public var artifactID: String public var version: String + public init(groupID: String, artifactID: String, version: String) { + self.groupID = groupID + self.artifactID = artifactID + self.version = version + } + public init(from decoder: any Decoder) throws { let container = try decoder.singleValueContainer() let string = try container.decode(String.self) @@ -124,26 +166,71 @@ public func readConfiguration(sourceDir: String, file: String = #fileID, line: U return try readConfiguration(configPath: configPath, file: file, line: line) } +/// Read a swift-java.config file at the specified path. +/// +/// Configuration is expected to be "JSON-with-comments". +/// Specifically "//" comments are allowed and will be trimmed before passing the rest of the config into a standard JSON parser. public func readConfiguration(configPath: URL, file: String = #fileID, line: UInt = #line) throws -> Configuration? { - guard let configData = try? Data(contentsOf: configPath) else { + let configData: Data + do { + configData = try Data(contentsOf: configPath) + } catch { + print("Failed to read SwiftJava configuration at '\(configPath.absoluteURL)', error: \(error)") + return nil + } + + guard let configString = String(data: configData, encoding: .utf8) else { + return nil + } + + return try readConfiguration(string: configString, configPath: configPath) +} + +public func readConfiguration(string: String, configPath: URL?, file: String = #fileID, line: UInt = #line) throws -> Configuration? { + guard let configData = string.data(using: .utf8) else { return nil } do { - return try JSONDecoder().decode(Configuration.self, from: configData) + let decoder = JSONDecoder() + decoder.allowsJSON5 = true + return try decoder.decode(Configuration.self, from: configData) } catch { - throw ConfigurationError(message: "Failed to parse SwiftJava configuration at '\(configPath.absoluteURL)'! \(#fileID):\(#line)", error: error, + throw ConfigurationError( + message: "Failed to parse SwiftJava configuration at '\(configPath.map({ $0.absoluteURL.description }) ?? "")'! \(#fileID):\(#line)", + error: error, + text: string, file: file, line: line) } } -public func findSwiftJavaClasspaths(moduleName: String) -> [String] { +/// Load all dependent configs configured with `--depends-on` and return a list of +/// `(SwiftModuleName, Configuration)` tuples. +public func loadDependentConfigs(dependsOn: [String]) throws -> [(String?, Configuration)] { + try dependsOn.map { dependentConfig in + let equalLoc = dependentConfig.firstIndex(of: "=") + + var swiftModuleName: String? = nil + if let equalLoc { + swiftModuleName = String(dependentConfig[.. [String] { let basePath: String = FileManager.default.currentDirectoryPath let pluginOutputsDir = URL(fileURLWithPath: basePath) .appendingPathComponent(".build", isDirectory: true) .appendingPathComponent("plugins", isDirectory: true) .appendingPathComponent("outputs", isDirectory: true) - .appendingPathComponent(moduleName, isDirectory: true) + .appendingPathComponent(swiftModule, isDirectory: true) return findSwiftJavaClasspaths(in: pluginOutputsDir.path) } @@ -154,7 +241,7 @@ public func findSwiftJavaClasspaths(in basePath: String = FileManager.default.cu let baseURL = URL(fileURLWithPath: basePath) var classpathEntries: [String] = [] - print("[debug][swift-java] Searching for *.swift-java.classpath files in: \(baseURL)") + print("[debug][swift-java] Searching for *.swift-java.classpath files in: \(baseURL.absoluteString)") guard let enumerator = fileManager.enumerator(at: baseURL, includingPropertiesForKeys: []) else { print("[warning][swift-java] Failed to get enumerator for \(baseURL)") return [] @@ -162,7 +249,7 @@ public func findSwiftJavaClasspaths(in basePath: String = FileManager.default.cu for case let fileURL as URL in enumerator { if fileURL.lastPathComponent.hasSuffix(".swift-java.classpath") { - print("[debug][swift-java] Constructing classpath with entries from: \(fileURL.relativePath)") + print("[debug][swift-java] Constructing classpath with entries from: \(fileURL.path)") if let contents = try? String(contentsOf: fileURL) { let entries = contents.split(separator: ":").map(String.init) for entry in entries { @@ -205,19 +292,21 @@ extension Configuration { public struct ConfigurationError: Error { let message: String let error: any Error + let text: String? let file: String let line: UInt - init(message: String, error: any Error, file: String = #fileID, line: UInt = #line) { + init(message: String, error: any Error, text: String?, file: String = #fileID, line: UInt = #line) { self.message = message self.error = error + self.text = text self.file = file self.line = line } } -public enum LogLevel: String, Codable, Hashable { +public enum LogLevel: String, ExpressibleByStringLiteral, Codable, Hashable { case trace = "trace" case debug = "debug" case info = "info" @@ -225,11 +314,15 @@ public enum LogLevel: String, Codable, Hashable { case warning = "warning" case error = "error" case critical = "critical" + + public init(stringLiteral value: String) { + self = LogLevel(rawValue: value) ?? .info + } } extension LogLevel { public init(from decoder: any Decoder) throws { - var container = try decoder.unkeyedContainer() + var container = try decoder.singleValueContainer() let string = try container.decode(String.self) switch string { case "trace": self = .trace diff --git a/Sources/SwiftJavaConfigurationShared/GradleDependencyParsing.swift b/Sources/SwiftJavaConfigurationShared/GradleDependencyParsing.swift new file mode 100644 index 000000000..bd4aada34 --- /dev/null +++ b/Sources/SwiftJavaConfigurationShared/GradleDependencyParsing.swift @@ -0,0 +1,48 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +// Regex is not sendable yet so we can't cache it in a let +fileprivate var GradleDependencyDescriptorRegex: Regex<(Substring, Substring, Substring, Substring)> { + try! Regex(#"^([^:]+):([^:]+):(\d[^:]+)$"#) // TODO: improve the regex to be more precise +} + +// note: can't use `package` access level since it would break in usage in plugins in `_PluginsShared`. +public func parseDependencyDescriptor(_ descriptor: String) -> JavaDependencyDescriptor? { + guard let match = try? GradleDependencyDescriptorRegex.firstMatch(in: descriptor) else { + return nil + } + + let groupID = String(match.1) + let artifactID = String(match.2) + let version = String(match.3) + + return JavaDependencyDescriptor(groupID: groupID, artifactID: artifactID, version: version) +} + +// note: can't use `package` access level since it would break in usage in plugins in `_PluginsShared`. +public func parseDependencyDescriptors(_ string: String) -> [JavaDependencyDescriptor] { + let descriptors = string.components(separatedBy: ",") + var parsedDependencies: [JavaDependencyDescriptor] = [] + parsedDependencies.reserveCapacity(descriptors.count) + + for descriptor in descriptors { + if let dependency = parseDependencyDescriptor(descriptor) { + parsedDependencies.append(dependency) + } + } + + return parsedDependencies +} \ No newline at end of file diff --git a/Sources/SwiftJavaConfigurationShared/JExtract/JExtractAsyncFuncMode.swift b/Sources/SwiftJavaConfigurationShared/JExtract/JExtractAsyncFuncMode.swift new file mode 100644 index 000000000..d7fd84623 --- /dev/null +++ b/Sources/SwiftJavaConfigurationShared/JExtract/JExtractAsyncFuncMode.swift @@ -0,0 +1,33 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +/// Configures how Swift `async` functions should be extracted by jextract. +public enum JExtractAsyncFuncMode: String, Codable { + /// Extract Swift `async` APIs as Java functions that return `CompletableFuture`s. + case completableFuture + + /// Extract Swift `async` APIs as Java functions that return `Future`s. + /// + /// This mode is useful for platforms that do not have `CompletableFuture` support, such as + /// Android 23 and below. + /// + /// - Note: Prefer using the `completableFuture` mode instead, if possible. + case legacyFuture +} + +extension JExtractAsyncFuncMode { + public static var `default`: Self { + .completableFuture + } +} diff --git a/Sources/SwiftJavaConfigurationShared/JExtract/JExtractGenerationMode.swift b/Sources/SwiftJavaConfigurationShared/JExtract/JExtractGenerationMode.swift new file mode 100644 index 000000000..8e11d82b0 --- /dev/null +++ b/Sources/SwiftJavaConfigurationShared/JExtract/JExtractGenerationMode.swift @@ -0,0 +1,26 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +/// Determines which source generation mode JExtract should be using: JNI or Foreign Function and Memory. +public enum JExtractGenerationMode: String, Sendable, Codable { + /// Foreign Value and Memory API + case ffm + + /// Java Native Interface + case jni + + public static var `default`: JExtractGenerationMode { + .ffm + } +} diff --git a/Sources/SwiftJavaConfigurationShared/JExtract/JExtractMemoryManagementMode.swift b/Sources/SwiftJavaConfigurationShared/JExtract/JExtractMemoryManagementMode.swift new file mode 100644 index 000000000..be77d27d3 --- /dev/null +++ b/Sources/SwiftJavaConfigurationShared/JExtract/JExtractMemoryManagementMode.swift @@ -0,0 +1,34 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +/// Configures how memory should be managed by the user +public enum JExtractMemoryManagementMode: String, Codable { + /// Force users to provide an explicit `SwiftArena` to all calls that require them. + case explicit + + /// Provide both explicit `SwiftArena` support + /// and a default global automatic `SwiftArena` that will deallocate memory when the GC decides to. + case allowGlobalAutomatic + + public static var `default`: Self { + .explicit + } + + public var requiresGlobalArena: Bool { + switch self { + case .explicit: false + case .allowGlobalAutomatic: true + } + } +} diff --git a/Sources/SwiftJavaConfigurationShared/JExtract/JExtractMinimumAccessLevelMode.swift b/Sources/SwiftJavaConfigurationShared/JExtract/JExtractMinimumAccessLevelMode.swift new file mode 100644 index 000000000..22fead577 --- /dev/null +++ b/Sources/SwiftJavaConfigurationShared/JExtract/JExtractMinimumAccessLevelMode.swift @@ -0,0 +1,26 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +/// The minimum access level which +public enum JExtractMinimumAccessLevelMode: String, Codable { + case `public` + case `package` + case `internal` +} + +extension JExtractMinimumAccessLevelMode { + public static var `default`: Self { + .public + } +} diff --git a/Sources/SwiftJavaConfigurationShared/JExtract/JExtractUnsignedIntegerMode.swift b/Sources/SwiftJavaConfigurationShared/JExtract/JExtractUnsignedIntegerMode.swift new file mode 100644 index 000000000..b53a2c6b7 --- /dev/null +++ b/Sources/SwiftJavaConfigurationShared/JExtract/JExtractUnsignedIntegerMode.swift @@ -0,0 +1,56 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +/// Configures how Swift unsigned integers should be extracted by jextract. +public enum JExtractUnsignedIntegerMode: String, Codable { + /// Treat unsigned Swift integers as their signed equivalents in Java signatures, + /// however annotate them using the `@Unsigned` annotation which serves as a hint + /// to users of APIs with unsigned integers that a given parameter or return type + /// is actually unsigned, and must be treated carefully. + /// + /// Specifically negative values of a `@Unchecked long` must be interpreted carefully as + /// a value larger than the Long.MAX_VALUE can represent in Java. + case annotate + + /// Wrap any unsigned Swift integer values in an explicit `Unsigned...` wrapper types. + /// + /// This mode trades off performance, due to needing to allocate the type-safe wrapper objects around + /// primitive values, however allows to retain static type information about the unsignedness of + /// unsigned number types in the Java side of generated bindings. + case wrapGuava + +// /// If possible, use a wider Java signed integer type to represent an Unsigned Swift integer type. +// /// For example, represent a Swift `UInt32` (width equivalent to Java `int`) as a Java signed `long`, +// /// because UInt32's max value is possible to be stored in a signed Java long (64bit). +// /// +// /// Since it is not possible to widen a value beyond 64bits (Java `long`), the Long type would be wrapped +// case widenOrWrap +// +// /// Similar to `widenOrWrap`, however instead of wrapping `UInt64` as an `UnsignedLong` in Java, +// /// only annotate it as `@Unsigned long`. +// case widenOrAnnotate +} + + +extension JExtractUnsignedIntegerMode { + public var needsConversion: Bool { + switch self { + case .annotate: false + case .wrapGuava: true + } + } + + public static var `default`: Self { + .annotate + } +} diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md new file mode 100644 index 000000000..7161a26c9 --- /dev/null +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md @@ -0,0 +1,347 @@ +# Supported Features + +Summary of features supported by the swift-java interoperability libraries and tools. + +## Overview + +SwiftJava supports both directions of interoperability, using Swift macros and source generation +(via the `swift-java wrap-java` command). + +### Java -> Swift + +It is possible to use SwiftJava macros and the `wrap-java` command to simplify implementing +Java `native` functions. SwiftJava simplifies the type conversions + +> tip: This direction of interoperability is covered in the WWDC2025 session 'Explore Swift and Java interoperability' +> around the [7-minute mark](https://youtu.be/QSHO-GUGidA?si=vUXxphTeO-CHVZ3L&t=448). + +| Feature | Macro support | +|--------------------------------------------------|-------------------------| +| Java `static native` method implemented by Swift | ✅ `@JavaImplementation` | +| **This list is very work in progress** | | + +### Swift -> Java + + +> tip: This direction of interoperability is covered in the WWDC2025 session 'Explore Swift and Java interoperability' +> around the [10-minute mark](https://youtu.be/QSHO-GUGidA?si=QyYP5-p2FL_BH7aD&t=616). + +| Java Feature | Macro support | +|----------------------------------------|---------------| +| Java `class` | ✅ | +| Java class inheritance | ✅ | +| Java `abstract class` | TODO | +| Java `enum` | ❌ | +| Java methods: `static`, member | ✅ `@JavaMethod` | +| **This list is very work in progress** | | + + +### JExtract – calling Swift from Java + +SwiftJava's `swift-java jextract` tool automates generating Java bindings from Swift sources. + +> tip: This direction of interoperability is covered in the WWDC2025 session 'Explore Swift and Java interoperability' +> around the [14-minute mark](https://youtu.be/QSHO-GUGidA?si=b9YUwAWDWFGzhRXN&t=842). + + +| Swift Feature | FFM | JNI | +|--------------------------------------------------------------------------------------|----------|-----| +| Initializers: `class`, `struct` | ✅ | ✅ | +| Optional Initializers / Throwing Initializers | ❌ | ✅ | +| Deinitializers: `class`, `struct` | ✅ | ✅ | +| `enum` | ❌ | ✅ | +| `actor` | ❌ | ❌ | +| Global Swift `func` | ✅ | ✅ | +| Class/struct member `func` | ✅ | ✅ | +| Throwing functions: `func x() throws` | ❌ | ✅ | +| Typed throws: `func x() throws(E)` | ❌ | ❌ | +| Stored properties: `var`, `let` (with `willSet`, `didSet`) | ✅ | ✅ | +| Computed properties: `var` (incl. `throws`) | ✅ / TODO | ✅ | +| Async functions `func async` and properties: `var { get async {} }` | ❌ | ✅ | +| Arrays: `[UInt8]`, `[MyType]`, `Array` etc | ❌ | ✅ | +| Dictionaries: `[String: Int]`, `[K:V]` | ❌ | ❌ | +| Generic parameters in functions: `func f(x: T)` | ❌ | ✅ | +| Generic return values in functions: `func f() -> T` | ❌ | ❌ | +| Tuples: `(Int, String)`, `(A, B, C)` | ❌ | ❌ | +| Protocols: `protocol` | ❌ | ✅ | +| Protocols: `protocol` with associated types | ❌ | ❌ | +| Existential parameters `f(x: any SomeProtocol) ` | ❌ | ✅ | +| Existential parameters `f(x: any (A & B)) ` | ❌ | ✅ | +| Existential return types `f() -> any Collection ` | ❌ | ❌ | +| Foundation Data and DataProtocol: `f(x: any DataProtocol) -> Data` | ✅ | ❌ | +| Opaque parameters: `func take(worker: some Builder) -> some Builder` | ❌ | ✅ | +| Opaque return types: `func get() -> some Builder` | ❌ | ❌ | +| Optional parameters: `func f(i: Int?, class: MyClass?)` | ✅ | ✅ | +| Optional return types: `func f() -> Int?`, `func g() -> MyClass?` | ❌ | ✅ | +| Primitive types: `Bool`, `Int`, `Int8`, `Int16`, `Int32`, `Int64`, `Float`, `Double` | ✅ | ✅ | +| Parameters: SwiftJava wrapped types `JavaLong`, `JavaInteger` | ❌ | ✅ | +| Return values: SwiftJava wrapped types `JavaLong`, `JavaInteger` | ❌ | ❌ | +| Unsigned primitive types: `UInt`, `UInt8`, `UInt16`, `UInt32`, `UInt64` | ✅ * | ✅ * | +| String (with copying data) | ✅ | ✅ | +| Variadic parameters: `T...` | ❌ | ❌ | +| Parametrer packs / Variadic generics | ❌ | ❌ | +| Ownership modifiers: `inout`, `borrowing`, `consuming` | ❌ | ❌ | +| Default parameter values: `func p(name: String = "")` | ❌ | ❌ | +| Operators: `+`, `-`, user defined | ❌ | ❌ | +| Subscripts: `subscript()` | ✅ | ✅ | +| Equatable | ❌ | ❌ | +| Pointers: `UnsafeRawPointer`, UnsafeBufferPointer (?) | 🟡 | ❌ | +| Nested types: `struct Hello { struct World {} }` | ❌ | ✅ | +| Inheritance: `class Caplin: Capybara` | ❌ | ❌ | +| Non-escaping `Void` closures: `func callMe(maybe: () -> ())` | ✅ | ✅ | +| Non-escaping closures with primitive arguments/results: `func callMe(maybe: (Int) -> (Double))` | ✅ | ✅ | +| Non-escaping closures with object arguments/results: `func callMe(maybe: (JavaObj) -> (JavaObj))` | ❌ | ❌ | +| `@escaping` closures: `func callMe(_: @escaping () -> ())` | ❌ | ❌ | +| Swift type extensions: `extension String { func uppercased() }` | ✅ | ✅ | +| Swift macros (maybe) | ❌ | ❌ | +| Result builders | ❌ | ❌ | +| Automatic Reference Counting of class types / lifetime safety | ✅ | ✅ | +| Value semantic types (e.g. struct copying) | ❌ | ❌ | +| | | | +| | | | + +> tip: The list of features may be incomplete, please file an issue if something is unclear or should be clarified in this table. + +## Detailed feature support discussion + +### Unsigned integers + +### Java <-> Swift Type mapping + +Java does not support unsigned numbers (other than the 16-bit wide `char`), and therefore mapping Swift's (and C) +unsigned integer types is somewhat problematic. + +SwiftJava's jextract mode, similar to OpenJDK jextract, does extract unsigned types from native code to Java +as their bit-width equivalents. This is potentially dangerous because values larger than the `MAX_VALUE` of a given +*signed* type in Java, e.g. `200` stored in an `UInt8` in Swift, would be interpreted as a `byte` of value `-56`, +because Java's `byte` type is _signed_. + +#### Unsigned numbers mode: annotate (default) + +Because in many situations the data represented by such numbers is merely passed along, and not interpreted by Java, +this may be safe to pass along. However, interpreting unsigned values incorrectly like this can lead to subtle mistakes +on the Java side. + +| Swift type | Java type | +|------------|-----------| +| `Int8` | `byte` | +| `UInt8` | `byte` ⚠️ | +| `Int16` | `short` | +| `UInt16` | `char` | +| `Int32` | `int` | +| `UInt32` | `int` ⚠️ | +| `Int64` | `long` | +| `UInt64` | `long` ⚠️ | +| `Float` | `float` | +| `Double` | `double` | + +#### Unsigned numbers mode: wrapGuava + +You can configure `jextract` (in FFM mode) to instead import unsigned values as their unsigned type-safe representations +as offered by the Guava library: `UnsignedLong` or `UnsignedInt`. To enable this mode pass the `--unsigned-numbers-mode wrapGuava` +command line option, or set the corresponding configuration value in `swift-java.config` (TODO). + +This approach is type-safe, however it incurs a performance penalty for allocating a wrapper class for every +unsigned integer parameter passed to and from native Swift functions. + +SwiftJava _does not_ vendor or provide the Guava library as a dependency, and when using this mode +you are expected to add a Guava dependency to your Java project. + +> You can read more about the unsigned integers support + +| Swift type | Java type | +|------------|--------------------------------------------------------| +| `Int8` | `byte` | +| `UInt8` | `com.google.common.primitives.UnsignedInteger` (class) | +| `Int16` | `short` | +| `UInt16` | `char` | +| `Int32` | `int` | +| `UInt32` | `com.google.common.primitives.UnsignedInteger` (class)️ | +| `Int64` | `long` | +| `UInt64` | `com.google.common.primitives.UnsignedLong` (class) | +| `Float` | `float` | +| `Double` | `double` | + +> Note: The `wrapGuava` mode is currently only available in FFM mode of jextract. + +### Enums + +> Note: Enums are currently only supported in JNI mode. + +Swift enums are extracted into a corresponding Java `class`. To support associated values +all cases are also extracted as Java `record`s. + +Consider the following Swift enum: +```swift +public enum Vehicle { + case car(String) + case bicycle(maker: String) +} +``` +You can then instantiate a case of `Vehicle` by using one of the static methods: +```java +try (var arena = SwiftArena.ofConfined()) { + Vehicle vehicle = Vehicle.car("BMW", arena); + Optional car = vehicle.getAsCar(); + assertEquals("BMW", car.orElseThrow().arg0()); +} +``` +As you can see above, to access the associated values of a case you can call one of the +`getAsX` methods that will return an Optional record with the associated values. +```java +try (var arena = SwiftArena.ofConfined()) { + Vehicle vehicle = Vehicle.bycicle("My Brand", arena); + Optional car = vehicle.getAsCar(); + assertFalse(car.isPresent()); + + Optional bicycle = vehicle.getAsBicycle(); + assertEquals("My Brand", bicycle.orElseThrow().maker()); +} +``` + +#### Switching and pattern matching + +If you only need to switch on the case and not access any associated values, +you can use the `getDiscriminator()` method: +```java +Vehicle vehicle = ...; +switch (vehicle.getDiscriminator()) { + case BICYCLE: + System.out.println("I am a bicycle!"); + break; + case CAR: + System.out.println("I am a car!"); + break; +} +``` +If you also want access to the associated values, you have various options +depending on the Java version you are using. +If you are running Java 21+ you can use [pattern matching for switch](https://openjdk.org/jeps/441): +```java +Vehicle vehicle = ...; +switch (vehicle.getCase()) { + case Vehicle.Bicycle b: + System.out.println("Bicycle maker: " + b.maker()); + break; + case Vehicle.Car c: + System.out.println("Car: " + c.arg0()); + break; +} +``` +or even, destructuring the records in the switch statement's pattern match directly: +```java +Vehicle vehicle = ...; +switch (vehicle.getCase()) { + case Vehicle.Car(var name, var unused): + System.out.println("Car: " + name); + break; + default: + break; +} +``` + +For Java 16+ you can use [pattern matching for instanceof](https://openjdk.org/jeps/394) +```java +Vehicle vehicle = ...; +Vehicle.Case case = vehicle.getCase(); +if (case instanceof Vehicle.Bicycle b) { + System.out.println("Bicycle maker: " + b.maker()); +} else if(case instanceof Vehicle.Car c) { + System.out.println("Car: " + c.arg0()); +} +``` +For any previous Java versions you can resort to casting the `Case` to the expected type: +```java +Vehicle vehicle = ...; +Vehicle.Case case = vehicle.getCase(); +if (case instanceof Vehicle.Bicycle) { + Vehicle.Bicycle b = (Vehicle.Bicycle) case; + System.out.println("Bicycle maker: " + b.maker()); +} else if(case instanceof Vehicle.Car) { + Vehicle.Car c = (Vehicle.Car) case; + System.out.println("Car: " + c.arg0()); +} +``` + +#### RawRepresentable enums + +JExtract also supports extracting enums that conform to `RawRepresentable` +by giving access to an optional initializer and the `rawValue` variable. +Consider the following example: +```swift +public enum Alignment: String { + case horizontal + case vertical +} +``` +you can then initialize `Alignment` from a `String` and also retrieve back its `rawValue`: +```java +try (var arena = SwiftArena.ofConfined()) { + Optional alignment = Alignment.init("horizontal", arena); + assertEqual(HORIZONTAL, alignment.orElseThrow().getDiscriminator()); + assertEqual("horizontal", alignment.orElseThrow().getRawValue()); +} +``` + +### Protocols + +> Note: Protocols are currently only supported in JNI mode. +> +> With the exception of `any DataProtocol` which is handled as `Foundation.Data` in the FFM mode. + +Swift `protocol` types are imported as Java `interface`s. For now, we require that all +concrete types of an interface wrap a Swift instance. In the future, we will add support +for providing Java-based implementations of interfaces, that you can pass to Java functions. + +Consider the following Swift protocol: +```swift +protocol Named { + var name: String { get } + + func describe() -> String +} +``` +will be exported as +```java +interface Named extends JNISwiftInstance { + public String getName(); + + public String describe(); +} +``` + +#### Parameters +Any opaque, existential or generic parameters are imported as Java generics. +This means that the following function: +```swift +func f(x: S, y: any C, z: some D) +``` +will be exported as +```java + void f(S x, T1 y, T2 z) +``` +On the Java side, only SwiftInstance implementing types may be passed; +so this isn't a way for compatibility with just any arbitrary Java interfaces, +but specifically, for allowing passing concrete binding types generated by jextract from Swift types +which conform a to a given Swift protocol. + +#### Returning protocol types +Protocols are not yet supported as return types. + +### `async` functions + +> Note: Importing `async` functions is currently only available in the JNI mode of jextract. + +Asynchronous functions in Swift can be extraced using different modes, which are explained below. + +#### Async function mode: completable-future (default) + +In this mode `async` functions in Swift are extracted as Java methods returning a `java.util.concurrent.CompletableFuture`. +This mode gives the most flexibility and should be prefered if your platform supports `CompletableFuture`. + +#### Async mode: future + +This is a mode for legacy platforms, where `CompletableFuture` is not available, such as Android 23 and below. +In this mode `async` functions in Swift are extracted as Java methods returning a `java.util.concurrent.Future`. +To enable this mode pass the `--async-func-mode future` command line option, +or set the `asyncFuncMode` configuration value in `swift-java.config` diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaCommandLineTool.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaCommandLineTool.md new file mode 100644 index 000000000..823713836 --- /dev/null +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaCommandLineTool.md @@ -0,0 +1,267 @@ +# swift-java command line tool + +The `swift-java` command line tool offers multiple ways to interact your Java interoperability enabled projects. + +## Overview + +The `swift-java` command line tool offers multiple modes which you can use to prepare your Swift and Java code to interact with eachother. + +The following sections will explain the modes in depth. When in doubt, you can always use the command line `--help` to get additional +guidance about the tool and available options: + +```bash +> swift-java --help + +USAGE: swift-java + +OPTIONS: + -h, --help Show help information. + +SUBCOMMANDS: + configure Configure and emit a swift-java.config file based on an input dependency or jar file + resolve Resolve dependencies and write the resulting swift-java.classpath file + wrap-java Wrap Java classes with corresponding Swift bindings. + jextract Wrap Swift functions and types with Java bindings, making them available to be called from Java + + See 'swift-java help ' for detailed help. +``` + +### Expose Java classes to Swift: swift-java wrap-java + +The `swift-java` is a Swift program that uses Java's runtime reflection facilities to translate the requested Java classes into their Swift projections. The output is a number of Swift source files, each of which corresponds to a +single Java class. The `swift-java` can be executed like this: + +``` +swift-java help wrap-java +``` + +to produce help output like the following: + +``` +USAGE: swift-java wrap-java [--output-directory ] [--input-swift ] [--log-level ] [--cp ...] [--filter-java-package ] --swift-module [--depends-on ...] [--swift-native-implementation ...] [--cache-directory ] [--swift-match-package-directory-structure ] + +ARGUMENTS: + Path to .jar file whose Java classes should be wrapped using Swift bindings + +OPTIONS: + -o, --output-directory + The directory in which to output generated SwiftJava configuration files. + --input-swift + Directory containing Swift files which should be extracted into Java bindings. Also known as 'jextract' mode. Must be paired with --output-java and --output-swift. + -l, --log-level + Configure the level of logs that should be printed (values: trace, debug, info, notice, warning, error, critical; default: log level) + --cp, --classpath Class search path of directories and zip/jar files from which Java classes can be loaded. + -f, --filter-java-package + While scanning a classpath, inspect only types included in this package + --swift-module + The name of the Swift module into which the resulting Swift types will be generated. + --depends-on + A swift-java configuration file for a given Swift module name on which this module depends, + e.g., JavaKitJar=Sources/JavaKitJar/swift-java.config. There should be one of these options + for each Swift module that this module depends on (transitively) that contains wrapped Java sources. + --swift-native-implementation + The names of Java classes whose declared native methods will be implemented in Swift. + --cache-directory + Cache directory for intermediate results and other outputs between runs + --swift-match-package-directory-structure + Match java package directory structure with generated Swift files (default: false) + -h, --help Show help information. + +``` + +For example, the `JavaKitJar` library is generated with this command line: + +```swift +swift-java wrap-java --swift-module JavaKitJar --depends-on SwiftJNI=Sources/SwiftJNI/swift-java.config -o Sources/JavaKitJar/generated Sources/JavaKitJar/swift-java.config +``` + +The `--swift-module JavaKitJar` parameter describes the name of the Swift module in which the code will be generated. + +The `--depends-on` option is followed by the swift-java configuration files for any library on which this Swift library depends. Each `--depends-on` option is of the form `=`, and tells swift-java which other Java classes have already been translated to Swift. For example, if your Java class uses `java.net.URL`, then you should include +`JavaKitNetwork`'s configuration file as a dependency here. + +The `-o` option specifies the output directory. Typically, this will be `Sources//generated` or similar to keep the generated Swift files separate from any hand-written ones. To see the output on the terminal rather than writing files to disk, pass `-` for this option. + +Finally, the command line should contain the `swift-java.config` file containing the list of classes that should be translated into Swift and their corresponding Swift type names. The tool will output a single `.swift` file for each class, along with warnings for any public API that cannot be translated into Swift. The most common warnings are due to missing Swift projections for Java classes. For example, here we have not translated (or provided the translation manifests for) the Java classes +`java.util.zip.ZipOutputStream` and `java.io.OutputStream`: + +``` +warning: Unable to translate 'java.util.jar.JarOutputStream' superclass: Java class 'java.util.zip.ZipOutputStream' has not been translated into Swift +warning: Unable to translate 'java.util.jar.JarOutputStream' constructor: Java class 'java.io.OutputStream' has not been translated into Swift +warning: Unable to translate 'java.util.jar.JarInputStream' method 'transferTo': Java class 'java.io.OutputStream' has not been translated into Swift +``` + +The result of such warnings is that certain information won't be statically available in Swift, e.g., the superclass won't be known (so we will assume it is `JavaObject`), or the specified constructors or methods won't be translated. If you don't need these APIs, the warnings can be safely ignored. The APIs can still be called dynamically via JNI. + +The `--jar` option changes the operation of `swift-java`. Instead of wrapping Java classes in Swift, it scans the given input Jar file to find all public classes and outputs a configuration file `swift-java.config` mapping all of the Java classes in the Jar file to Swift types. The `--jar` mode is expected to be used to help import a Java library into Swift wholesale, after which swift-java should invoked again given the generated configuration file. + +### Under construction: Create a Java class to wrap the Swift library + +**NOTE**: the instructions here work, but we are still smoothing out the interoperability story. + +All JavaKit-based applications start execution within the Java Virtual Machine. First, define your own Java class that loads your native Swift library and provides a `native` entry point to get into the Swift code. Here is a minimal Java class that has all of the program's logic written in Swift, including `main`: + + +```java +package org.swift.javakit; + +public class HelloSwiftMain { + static { + System.loadLibrary("HelloSwift"); + } + + public native static void main(String[] args); +} +``` + +Compile this into a `.class` file with `javac` before we build the Swift half, e.g.,: + +``` +javac Java/src/org/swift/javakit/JavaClassTranslator.java +``` + +### Create a Swift library + +The Java class created above loads a native library `HelloSwift` that needs to contain a definition of the `main` method in the class `org.swift.javakit.HelloSwiftMain`. `HelloSwift` should be defined as a SwiftPM dynamic library product, e.g., + +```swift + products: [ + .library( + name: "HelloSwift", + type: .dynamic, + targets: ["HelloSwift"] + ), + ] +``` + +with an associated target that depends on `JavaKit`: + +```swift + .target( + name: "HelloSwift", + dependencies: [ + .product(name: "ArgumentParser", package: "swift-argument-parser"), + .product(name: "JavaKit", package: "JavaKit") + ]) +``` + +### Implement the `native` Java method in Swift +Now, in the `HelloSwift` Swift library, define a `struct` that provides the `main` method for the Java class we already defined: + +```swift +import SwiftJNI + +@JavaImplementation("org.swift.javakit.HelloSwiftMain") +struct HelloSwiftMain { + @JavaStaticMethod + static func main(arguments: [String], environment: JNIEnvironment? = nil) { + print("Command line arguments are: \(arguments)") + } +} +``` + +Go ahead and build this library with `swift build`, and find the path to the directory containing the resulting shared library (e.g., `HelloSwift.dylib`, `HelloSwift.so`, or `HelloSwift.dll`, depending on platform). It is often in `.build/debug/` if you ran `swift build` on the command line. + +### Putting it all together! + +Finally, run this program on the command line like this: + +``` +java -cp Java/src -Djava.library.path=$(PATH_CONTAINING_HELLO_SWIFT)/ org.swift.javakit.HelloSwiftMain -v argument +``` + +This will prints the command-line arguments `-v` and `argument` as seen by Swift. + +### Bonus: Swift argument parser + +The easiest way to build a command-line program in Swift is with the [Swift argument parser library](https://github.com/apple/swift-argument-parser). We can extend our `HelloSwiftMain` type to conform to `ParsableCommand` and using the Swift argument parser to process the arguments provided by Java: + +```swift +import ArgumentParser +import SwiftJNI + +@JavaClass("org.swift.jni.HelloSwiftMain") +struct HelloSwiftMain: ParsableCommand { + @Option(name: .shortAndLong, help: "Enable verbose output") + var verbose: Bool = false + + @JavaImplementation + static func main(arguments: [String], environment: JNIEnvironment? = nil) { + let command = Self.parseOrExit(arguments) + command.run(environment: environment) + } + + func run(environment: JNIEnvironment? = nil) { + print("Verbose = \(verbose)") + } +} +``` + +### Download Java dependencies in Swift builds: swift-java resolve + +> TIP: See the `Samples/DependencySampleApp` for a fully functional showcase of this mode. + +TODO: documentation on this feature + +### Expose Swift code to Java: swift-java jextract + +The project is still very early days, however the general outline of using this approach is as follows: + +- **No code changes** need to be made to Swift libraries that are to be exposed to Java using jextract-swift. +- Swift sources are compiled to `.swiftinterface` files +- These `.swiftinterface` files are imported by jextract-swift which generates `*.java` files +- The generated Java files contain generated code for efficient native invocations. + +You can then use Swift libraries in Java just by calling the appropriate methods and initializers. + +### Generating Java bindings for Swift libraries + +This repository also includes the `jextract-swift` tool which is similar to the JDK's [`jextract`](https://github.com/openjdk/jextract/). + +This approach offers two modes of operation: + +- the default `--mode ffm` which uses the [JEP-424 Foreign function and Memory APIs](https://openjdk.org/jeps/424) which are available since JDK **22**. It promises much higher performance than traditional approaches using JNI, and is primarily aimed for calling native code from a Java application. + +> Tip: In order to use the ffm mode, you need to install a recent enough JDK (at least JDK 22). The recommended, and simplest way, to install the a JDK distribution of your choice is [sdkman](https://sdkman.io): +> +> ``` +> curl -s "https://get.sdkman.io" | bash +> sdk install java 22-open +> +> export JAVA_HOME=$(sdk home java 22-open) +> ``` + +`jextract-swift` can be pointed at `*.swiftinterface` files and will generate corresponding Java files that use the (new in Java 22) Foreign Function & Memory APIs to expose efficient ways to call "down" into Swift from Java. + +### Default jextract behaviors + +Only `public` functions, properties and types are imported. + +Global Swift functions become static functions on on a class with the same name as the Swift module in Java, + +```swift +// Swift (Sources/SomeModule/Example.swift) + +public func globalFunction() +``` + +becomes: + +```java +// Java (SomeModule.java) + +public final class SomeModule ... { + public static void globalFunction() { ... } +} +``` + +### The swift-java.config file + +Many of the tools–as well as SwiftPM plugin's–behaviors can be configured using the `swift-java.config` file. + +You can refer to the `SwiftJavaConfigurationShared/Configuration` struct to learn about the supported options. + +Configuration from the config files may be overriden or augmented by explicit command line parameters, +please refer to the options documentation for details on their behavior. + +> Note: **Comments in configuration**: The configuration is a JSON 5 file, which among other things allows `//` and `/* */` comments, so feel free to add line comments explaining rationale for some of the settings in youf configuration. \ No newline at end of file diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftPMPlugin.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftPMPlugin.md new file mode 100644 index 000000000..d10ab387d --- /dev/null +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftPMPlugin.md @@ -0,0 +1,126 @@ +# SwiftJava SwiftPM Plugin + +The `SwiftJavaPlugin` automates `swift-java` command line tool invocations during the build process. + +## Overview + +### Installing the plugin + +To install the SwiftPM plugin in your target of choice include the `swift-java` package dependency: + +```swift +import Foundation + +let javaHome = findJavaHome() + +let javaIncludePath = "\(javaHome)/include" +#if os(Linux) + let javaPlatformIncludePath = "\(javaIncludePath)/linux" +#elseif os(macOS) + let javaPlatformIncludePath = "\(javaIncludePath)/darwin" +#elseif os(Windows) + let javaPlatformIncludePath = "\(javaIncludePath)/win32" +#endif + +let package = Package( + name: "MyProject", + + products: [ + .library( + name: "JavaKitExample", + type: .dynamic, + targets: ["JavaKitExample"] + ), + ], + + dependencies: [ + .package(url: "https://github.com/apple/swift-java", from: "..."), + ], + + targets: [ + .target( + name: "MyProject", + dependencies: [ + // ... + ], + swiftSettings: [ + .unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"]) + ], + plugins: [ + .plugin(name: "JavaCompilerPlugin", package: "swift-java"), + .plugin(name: "JExtractSwiftPlugin", package: "swift-java"), + .plugin(name: "SwiftJavaPlugin", package: "swift-java"), + ] + ), + ] +) +``` + +```swift + +// Note: the JAVA_HOME environment variable must be set to point to where +// Java is installed, e.g., +// Library/Java/JavaVirtualMachines/openjdk-21.jdk/Contents/Home. +func findJavaHome() -> String { + if let home = ProcessInfo.processInfo.environment["JAVA_HOME"] { + print("JAVA_HOME = \(home)") + return home + } + + // This is a workaround for envs (some IDEs) which have trouble with + // picking up env variables during the build process + let path = "\(FileManager.default.homeDirectoryForCurrentUser.path()).java_home" + if let home = try? String(contentsOfFile: path, encoding: .utf8) { + if let lastChar = home.last, lastChar.isNewline { + return String(home.dropLast()) + } + + return home + } + + if let home = getJavaHomeFromLibexecJavaHome(), + !home.isEmpty { + return home + } + + fatalError("Please set the JAVA_HOME environment variable to point to where Java is installed.") +} + +/// On MacOS we can use the java_home tool as a fallback if we can't find JAVA_HOME environment variable. +func getJavaHomeFromLibexecJavaHome() -> String? { + let task = Process() + task.executableURL = URL(fileURLWithPath: "/usr/libexec/java_home") + + // Check if the executable exists before trying to run it + guard FileManager.default.fileExists(atPath: task.executableURL!.path) else { + print("/usr/libexec/java_home does not exist") + return nil + } + + let pipe = Pipe() + task.standardOutput = pipe + task.standardError = pipe // Redirect standard error to the same pipe for simplicity + + do { + try task.run() + task.waitUntilExit() + + let data = pipe.fileHandleForReading.readDataToEndOfFile() + let output = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) + + if task.terminationStatus == 0 { + return output + } else { + print("java_home terminated with status: \(task.terminationStatus)") + // Optionally, log the error output for debugging + if let errorOutput = String(data: pipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) { + print("Error output: \(errorOutput)") + } + return nil + } + } catch { + print("Error running java_home: \(error)") + return nil + } +} +``` \ No newline at end of file diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/index.md b/Sources/SwiftJavaDocumentation/Documentation.docc/index.md new file mode 100644 index 000000000..4383727d9 --- /dev/null +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/index.md @@ -0,0 +1,44 @@ +# ``SwiftJavaDocumentation`` + +The SwiftJava project enables interoperability between Swift and Java. + +## Overview + +This project contains a number of support packages, java libraries, tools and plugins that provide a complete +Swift and Java interoperability story. + +Please refer to articles about the specific direction of interoperability you are interested in. + +### Getting started + +**SwiftJava** provides a set of tools and libraries to enable Java and Swift interoperability. It allows developers to generate bindings to either language from the other, by using either source generation (for Java consuming Swift code) or a combination of Swift macros and source generation (for Swift consuming Java libraries). + +The generated code is highly efficient and less error-prone than manually mapping, and also guarantees memory safety across the boundaries between the languages. + +Reasons why you might want to reach for Swift and Java interoperability include, but are not limited to, the following scenarios: +- Incremental adoption of Swift in an existing Java codebase +- Reuse existing libraries which exist in one ecosystem, but don't have a direct equivalent in the other + +SwiftJava is offering several core libraries which support language interoperability: +- `SwiftJava` (Swift -> Java) - JNI-based support library and Swift macros +- `SwiftKit` (Java -> Swift) - Support library for Java calling Swift code (either using JNI or FFM) +- `swift-java` - command line tool; Supports source generation and also dependency management operations +- Build tool integration - SwiftPM Plugin + +If you prefer a video introduction, you may want to watch this +[Explore Swift and Java interoperability](https://www.youtube.com/watch?v=QSHO-GUGidA) +WWDC 2025 session, +which is a quick overview of all the features and approaches offered by SwiftJava. + +## Topics + +### Supported Features + +- + + +### Source Generation + +- +- + diff --git a/Sources/SwiftJavaDocumentation/empty.swift b/Sources/SwiftJavaDocumentation/empty.swift new file mode 100644 index 000000000..9c28861c3 --- /dev/null +++ b/Sources/SwiftJavaDocumentation/empty.swift @@ -0,0 +1,15 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// Empty file, this target is a docs-only target. diff --git a/Sources/JavaKitMacros/GenerationMode.swift b/Sources/SwiftJavaMacros/GenerationMode.swift similarity index 100% rename from Sources/JavaKitMacros/GenerationMode.swift rename to Sources/SwiftJavaMacros/GenerationMode.swift diff --git a/Sources/JavaKitMacros/ImplementsJavaMacro.swift b/Sources/SwiftJavaMacros/ImplementsJavaMacro.swift similarity index 100% rename from Sources/JavaKitMacros/ImplementsJavaMacro.swift rename to Sources/SwiftJavaMacros/ImplementsJavaMacro.swift diff --git a/Sources/JavaKitMacros/JavaClassMacro.swift b/Sources/SwiftJavaMacros/JavaClassMacro.swift similarity index 100% rename from Sources/JavaKitMacros/JavaClassMacro.swift rename to Sources/SwiftJavaMacros/JavaClassMacro.swift diff --git a/Sources/JavaKitMacros/JavaFieldMacro.swift b/Sources/SwiftJavaMacros/JavaFieldMacro.swift similarity index 100% rename from Sources/JavaKitMacros/JavaFieldMacro.swift rename to Sources/SwiftJavaMacros/JavaFieldMacro.swift diff --git a/Sources/JavaKitMacros/JavaMethodMacro.swift b/Sources/SwiftJavaMacros/JavaMethodMacro.swift similarity index 64% rename from Sources/JavaKitMacros/JavaMethodMacro.swift rename to Sources/SwiftJavaMacros/JavaMethodMacro.swift index db8a3b367..099484528 100644 --- a/Sources/JavaKitMacros/JavaMethodMacro.swift +++ b/Sources/SwiftJavaMacros/JavaMethodMacro.swift @@ -47,18 +47,61 @@ extension JavaMethodMacro: BodyMacro { } guard let funcDecl = declaration.as(FunctionDeclSyntax.self) else { - fatalError("not a function") + fatalError("not a function: \(declaration)") } + let funcName = + if case .argumentList(let arguments) = node.arguments, + let argument = arguments.first, + argument.label?.text != "typeErasedResult", + let stringLiteral = argument.expression.as(StringLiteralExprSyntax.self), + stringLiteral.segments.count == 1, + case let .stringSegment(funcNameSegment)? = stringLiteral.segments.first + { + funcNameSegment.content.text + } else { + funcDecl.name.text + } + let isStatic = node.attributeName.trimmedDescription == "JavaStaticMethod" - let funcName = funcDecl.name.text let params = funcDecl.signature.parameterClause.parameters - let resultType: String = - funcDecl.signature.returnClause.map { result in - ", resultType: \(result.type.typeReferenceString).self" - } ?? "" let paramNames = params.map { param in param.parameterName?.text ?? "" }.joined(separator: ", ") + let genericResultType: String? = + if case let .argumentList(arguments) = node.arguments, + let element = arguments.first(where: { $0.label?.text == "typeErasedResult" }), + let stringLiteral = element.expression + .as(StringLiteralExprSyntax.self), + stringLiteral.segments.count == 1, + case let .stringSegment(wrapperName)? = stringLiteral.segments.first { + // TODO: Improve this unwrapping a bit; + // Trim the trailing ! and ? from the type for purposes + // of initializing the type wrapper in the method body + if "\(wrapperName)".hasSuffix("!") || + "\(wrapperName)".hasSuffix("?") { + String("\(wrapperName)".dropLast()) + } else { + "\(wrapperName)" + } + } else { + nil + } + + // Determine the result type + let resultType: String = + if let returnClause = funcDecl.signature.returnClause { + if let genericResultType { + // we need to type-erase the signature, because on JVM level generics are erased and we'd otherwise + // form a signature with the "concrete" type, which would not match the real byte-code level signature + // of the method we're trying to call -- which would result in a MethodNotFound exception. + ", resultType: /*type-erased:\(genericResultType)*/JavaObject?.self" + } else { + ", resultType: \(returnClause.type.typeReferenceString).self" + } + } else { + "" + } + let parametersAsArgs: String if paramNames.isEmpty { parametersAsArgs = "" @@ -70,8 +113,25 @@ extension JavaMethodMacro: BodyMacro { funcDecl.signature.effectSpecifiers?.throwsClause != nil ? "try" : "try!" + let resultSyntax: CodeBlockItemSyntax = + "\(raw: tryKeyword) dynamicJava\(raw: isStatic ? "Static" : "")MethodCall(methodName: \(literal: funcName)\(raw: parametersAsArgs)\(raw: resultType))" + + if let genericResultType { + return [ + """ + /* convert erased return value to \(raw: genericResultType) */ + if let result$ = \(resultSyntax) { + return \(raw: genericResultType)(javaThis: result$.javaThis, environment: try! JavaVirtualMachine.shared().environment()) + } else { + return nil + } + """ + ] + } + + // no return type conversions return [ - "return \(raw: tryKeyword) dynamicJava\(raw: isStatic ? "Static" : "")MethodCall(methodName: \(literal: funcName)\(raw: parametersAsArgs)\(raw: resultType))" + "return \(resultSyntax)" ] } diff --git a/Sources/JavaKitMacros/MacroErrors.swift b/Sources/SwiftJavaMacros/MacroErrors.swift similarity index 100% rename from Sources/JavaKitMacros/MacroErrors.swift rename to Sources/SwiftJavaMacros/MacroErrors.swift diff --git a/Sources/JavaKitMacros/JavaKitMacrosPlugin.swift b/Sources/SwiftJavaMacros/SwiftJNIMacrosPlugin.swift similarity index 93% rename from Sources/JavaKitMacros/JavaKitMacrosPlugin.swift rename to Sources/SwiftJavaMacros/SwiftJNIMacrosPlugin.swift index 255e61f5d..637b2ea40 100644 --- a/Sources/JavaKitMacros/JavaKitMacrosPlugin.swift +++ b/Sources/SwiftJavaMacros/SwiftJNIMacrosPlugin.swift @@ -16,7 +16,7 @@ import SwiftCompilerPlugin import SwiftSyntaxMacros @main -struct JavaKitMacrosPlugin: CompilerPlugin { +struct SwiftJavaMacrosPlugin: CompilerPlugin { var providingMacros: [Macro.Type] = [ JavaImplementationMacro.self, JavaClassMacro.self, diff --git a/Sources/JavaKitMacros/SwiftSyntaxUtils.swift b/Sources/SwiftJavaMacros/SwiftSyntaxUtils.swift similarity index 100% rename from Sources/JavaKitMacros/SwiftSyntaxUtils.swift rename to Sources/SwiftJavaMacros/SwiftSyntaxUtils.swift diff --git a/Sources/SwiftJavaRuntimeSupport/JNI.swift b/Sources/SwiftJavaRuntimeSupport/JNI.swift new file mode 100644 index 000000000..b6f0117df --- /dev/null +++ b/Sources/SwiftJavaRuntimeSupport/JNI.swift @@ -0,0 +1,31 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftJava +import CSwiftJavaJNI + +final class JNI { + static var shared: JNI! + + let applicationClassLoader: JavaClassLoader + + init(fromVM javaVM: JavaVirtualMachine) { + self.applicationClassLoader = try! JavaClass(environment: javaVM.environment()).currentThread().getContextClassLoader() + } +} + +// Called by generated code, and not automatically by Java. +public func _JNI_OnLoad(_ javaVM: JavaVMPointer, _ reserved: UnsafeMutableRawPointer) { + JNI.shared = JNI(fromVM: JavaVirtualMachine(adoptingJVM: javaVM)) +} diff --git a/Sources/SwiftJavaRuntimeSupport/JNIMethodIDCaches.swift b/Sources/SwiftJavaRuntimeSupport/JNIMethodIDCaches.swift new file mode 100644 index 000000000..7be79a9fa --- /dev/null +++ b/Sources/SwiftJavaRuntimeSupport/JNIMethodIDCaches.swift @@ -0,0 +1,123 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftJava + +extension _JNIMethodIDCache { + public enum CompletableFuture { + private static let completeMethod = Method( + name: "complete", + signature: "(Ljava/lang/Object;)Z" + ) + + private static let completeExceptionallyMethod = Method( + name: "completeExceptionally", + signature: "(Ljava/lang/Throwable;)Z" + ) + + private static let cache = _JNIMethodIDCache( + className: "java/util/concurrent/CompletableFuture", + methods: [completeMethod, completeExceptionallyMethod] + ) + + public static var `class`: jclass { + cache.javaClass + } + + /// CompletableFuture.complete(T) + public static var complete: jmethodID { + cache.methods[completeMethod]! + } + + /// CompletableFuture.completeExceptionally(Throwable) + public static var completeExceptionally: jmethodID { + cache.methods[completeExceptionallyMethod]! + } + } + + public enum SimpleCompletableFuture { + private static let completeMethod = Method( + name: "complete", + signature: "(Ljava/lang/Object;)Z" + ) + + private static let completeExceptionallyMethod = Method( + name: "completeExceptionally", + signature: "(Ljava/lang/Throwable;)Z" + ) + + private static let cache = _JNIMethodIDCache( + className: "org/swift/swiftkit/core/SimpleCompletableFuture", + methods: [completeMethod, completeExceptionallyMethod] + ) + + public static var `class`: jclass { + cache.javaClass + } + + public static var complete: jmethodID { + cache.methods[completeMethod]! + } + + public static var completeExceptionally: jmethodID { + cache.methods[completeExceptionallyMethod]! + } + } + + public enum Exception { + private static let messageConstructor = Method(name: "", signature: "(Ljava/lang/String;)V") + + private static let cache = _JNIMethodIDCache( + className: "java/lang/Exception", + methods: [messageConstructor] + ) + + public static var `class`: jclass { + cache.javaClass + } + + public static var constructWithMessage: jmethodID { + cache.methods[messageConstructor]! + } + } + + public enum JNISwiftInstance { + private static let memoryAddressMethod = Method( + name: "$memoryAddress", + signature: "()J" + ) + + private static let typeMetadataAddressMethod = Method( + name: "$typeMetadataAddress", + signature: "()J" + ) + + private static let cache = _JNIMethodIDCache( + className: "org/swift/swiftkit/core/JNISwiftInstance", + methods: [memoryAddressMethod, typeMetadataAddressMethod] + ) + + public static var `class`: jclass { + cache.javaClass + } + + public static var memoryAddress: jmethodID { + cache.methods[memoryAddressMethod]! + } + + public static var typeMetadataAddress: jmethodID { + cache.methods[typeMetadataAddressMethod]! + } + } +} diff --git a/Sources/SwiftJavaRuntimeSupport/_JNIBoxedConversions.swift b/Sources/SwiftJavaRuntimeSupport/_JNIBoxedConversions.swift new file mode 100644 index 000000000..ad4572caa --- /dev/null +++ b/Sources/SwiftJavaRuntimeSupport/_JNIBoxedConversions.swift @@ -0,0 +1,97 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import CSwiftJavaJNI +import SwiftJava + +public enum _JNIBoxedConversions { + private static let booleanMethod = _JNIMethodIDCache.Method(name: "valueOf", signature: "(Z)Ljava/lang/Boolean;", isStatic: true) + private static let byteMethod = _JNIMethodIDCache.Method(name: "valueOf", signature: "(B)Ljava/lang/Byte;", isStatic: true) + private static let charMethod = _JNIMethodIDCache.Method(name: "valueOf", signature: "(C)Ljava/lang/Character;", isStatic: true) + private static let shortMethod = _JNIMethodIDCache.Method(name: "valueOf", signature: "(S)Ljava/lang/Short;", isStatic: true) + private static let intMethod = _JNIMethodIDCache.Method(name: "valueOf", signature: "(I)Ljava/lang/Integer;", isStatic: true) + private static let longMethod = _JNIMethodIDCache.Method(name: "valueOf", signature: "(J)Ljava/lang/Long;", isStatic: true) + private static let floatMethod = _JNIMethodIDCache.Method(name: "valueOf", signature: "(F)Ljava/lang/Float;", isStatic: true) + private static let doubleMethod = _JNIMethodIDCache.Method(name: "valueOf", signature: "(D)Ljava/lang/Double;", isStatic: true) + + private static let booleanCache = _JNIMethodIDCache( + className: "java/lang/Boolean", + methods: [booleanMethod] + ) + + private static let byteCache = _JNIMethodIDCache( + className: "java/lang/Byte", + methods: [byteMethod] + ) + private static let charCache = _JNIMethodIDCache( + className: "java/lang/Character", + methods: [charMethod] + ) + + private static let shortCache = _JNIMethodIDCache( + className: "java/lang/Short", + methods: [shortMethod] + ) + private static let intCache = _JNIMethodIDCache( + className: "java/lang/Integer", + methods: [intMethod] + ) + + private static let longCache = _JNIMethodIDCache( + className: "java/lang/Long", + methods: [longMethod] + ) + + private static let floatCache = _JNIMethodIDCache( + className: "java/lang/Float", + methods: [floatMethod] + ) + + private static let doubleCache = _JNIMethodIDCache( + className: "java/lang/Double", + methods: [doubleMethod] + ) + + public static func box(_ value: jboolean, in env: JNIEnvironment) -> jobject { + env.interface.CallStaticObjectMethodA(env, booleanCache.javaClass, booleanCache.methods[booleanMethod]!, [jvalue(z: value)])! + } + + public static func box(_ value: jbyte, in env: JNIEnvironment) -> jobject { + env.interface.CallStaticObjectMethodA(env, byteCache.javaClass, byteCache.methods[byteMethod]!, [jvalue(b: value)])! + } + + public static func box(_ value: jchar, in env: JNIEnvironment) -> jobject { + env.interface.CallStaticObjectMethodA(env, charCache.javaClass, charCache.methods[charMethod]!, [jvalue(c: value)])! + } + + public static func box(_ value: jshort, in env: JNIEnvironment) -> jobject { + env.interface.CallStaticObjectMethodA(env, shortCache.javaClass, shortCache.methods[shortMethod]!, [jvalue(s: value)])! + } + + public static func box(_ value: jint, in env: JNIEnvironment) -> jobject { + env.interface.CallStaticObjectMethodA(env, intCache.javaClass, intCache.methods[intMethod]!, [jvalue(i: value)])! + } + + public static func box(_ value: jlong, in env: JNIEnvironment) -> jobject { + env.interface.CallStaticObjectMethodA(env, longCache.javaClass, longCache.methods[longMethod]!, [jvalue(j: value)])! + } + + public static func box(_ value: jfloat, in env: JNIEnvironment) -> jobject { + env.interface.CallStaticObjectMethodA(env, floatCache.javaClass, floatCache.methods[floatMethod]!, [jvalue(f: value)])! + } + + public static func box(_ value: jdouble, in env: JNIEnvironment) -> jobject { + env.interface.CallStaticObjectMethodA(env, doubleCache.javaClass, doubleCache.methods[doubleMethod]!, [jvalue(d: value)])! + } +} diff --git a/Sources/SwiftJavaRuntimeSupport/_JNIMethodIDCache.swift b/Sources/SwiftJavaRuntimeSupport/_JNIMethodIDCache.swift new file mode 100644 index 000000000..fbadf1916 --- /dev/null +++ b/Sources/SwiftJavaRuntimeSupport/_JNIMethodIDCache.swift @@ -0,0 +1,93 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import CSwiftJavaJNI +import SwiftJava + +/// A cache used to hold references for JNI method and classes. +/// +/// This type is used internally in by the outputted JExtract wrappers +/// to improve performance of any JNI lookups. +public final class _JNIMethodIDCache: Sendable { + public struct Method: Hashable { + public let name: String + public let signature: String + public let isStatic: Bool + + public init(name: String, signature: String, isStatic: Bool = false) { + self.name = name + self.signature = signature + self.isStatic = isStatic + } + } + + nonisolated(unsafe) let _class: jclass? + nonisolated(unsafe) let methods: [Method: jmethodID] + + public var javaClass: jclass { + self._class! + } + + /// An optional reference to a java object holder + /// if we cached this class through the class loader + /// This is to make sure that the underlying reference remains valid + nonisolated(unsafe) private let javaObjectHolder: JavaObjectHolder? + + public init(className: String, methods: [Method]) { + let environment = try! JavaVirtualMachine.shared().environment() + + let clazz: jobject + if let jniClass = environment.interface.FindClass(environment, className) { + clazz = environment.interface.NewGlobalRef(environment, jniClass)! + self.javaObjectHolder = nil + } else { + // Clear any ClassNotFound exceptions from FindClass + environment.interface.ExceptionClear(environment) + + if let javaClass = try? JNI.shared.applicationClassLoader.loadClass( + className.replacingOccurrences(of: "/", with: ".") + ) { + clazz = javaClass.javaThis + self.javaObjectHolder = javaClass.javaHolder + } else { + fatalError("Class \(className) could not be found!") + } + } + + self._class = clazz + self.methods = methods.reduce(into: [:]) { (result, method) in + if method.isStatic { + if let methodID = environment.interface.GetStaticMethodID(environment, clazz, method.name, method.signature) { + result[method] = methodID + } else { + fatalError("Static method \(method.signature) with signature \(method.signature) not found in class \(className)") + } + } else { + if let methodID = environment.interface.GetMethodID(environment, clazz, method.name, method.signature) { + result[method] = methodID + } else { + fatalError("Method \(method.signature) with signature \(method.signature) not found in class \(className)") + } + } + } + } + + public subscript(_ method: Method) -> jmethodID? { + methods[method] + } + + public func cleanup(environment: UnsafeMutablePointer!) { + environment.interface.DeleteGlobalRef(environment, self._class) + } +} diff --git a/SwiftKit/src/main/java/org/swift/swiftkit/ManagedSwiftType.java b/Sources/SwiftJavaRuntimeSupport/generated/JavaJNISwiftInstance.swift similarity index 67% rename from SwiftKit/src/main/java/org/swift/swiftkit/ManagedSwiftType.java rename to Sources/SwiftJavaRuntimeSupport/generated/JavaJNISwiftInstance.swift index da25ae4d6..4040d0bcb 100644 --- a/SwiftKit/src/main/java/org/swift/swiftkit/ManagedSwiftType.java +++ b/Sources/SwiftJavaRuntimeSupport/generated/JavaJNISwiftInstance.swift @@ -12,14 +12,10 @@ // //===----------------------------------------------------------------------===// -package org.swift.swiftkit; - -import java.lang.foreign.MemorySegment; - -public interface ManagedSwiftType { - /** - * The memory segment of `self` of the managed Swift object/value. - */ - public MemorySegment $memorySegment(); +import SwiftJava +@JavaInterface("org.swift.swiftkit.core.JNISwiftInstance") +public struct JavaJNISwiftInstance { + @JavaMethod("$memoryAddress") + public func memoryAddress() -> Int64 } diff --git a/Sources/JavaKitShared/TerminalColors.swift b/Sources/SwiftJavaShared/TerminalColors.swift similarity index 100% rename from Sources/JavaKitShared/TerminalColors.swift rename to Sources/SwiftJavaShared/TerminalColors.swift diff --git a/Sources/SwiftJavaTool/Commands/ConfigureCommand.swift b/Sources/SwiftJavaTool/Commands/ConfigureCommand.swift new file mode 100644 index 000000000..05bf3b8f3 --- /dev/null +++ b/Sources/SwiftJavaTool/Commands/ConfigureCommand.swift @@ -0,0 +1,252 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Logging +import ArgumentParser +import Foundation +import SwiftJavaToolLib +import JExtractSwiftLib +import SwiftJava +import JavaUtilJar +import JavaNet +import JavaLangReflect +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftJavaConfigurationShared +import SwiftJavaShared + +extension SwiftJava { + struct ConfigureCommand: SwiftJavaBaseAsyncParsableCommand, HasCommonOptions, HasCommonJVMOptions { + + static let log: Logging.Logger = Logger(label: "swift-java:\(configuration.commandName!)") + + static let configuration = CommandConfiguration( + commandName: "configure", + abstract: "Configure and emit a swift-java.config file based on an input dependency or jar file") + + @OptionGroup var commonOptions: SwiftJava.CommonOptions + @OptionGroup var commonJVMOptions: SwiftJava.CommonJVMOptions + + // TODO: This should be a "make wrappers" option that just detects when we give it a jar + @Flag(help: "Specifies that the input is a *.jar file whose public classes will be loaded. The output of swift-java will be a configuration file (swift-java.config) that can be used as input to a subsequent swift-java invocation to generate wrappers for those public classes.") + var jar: Bool = false + + @Option( + name: .long, + help: "How to handle an existing swift-java.config; by default 'overwrite' by can be changed to amending a configuration" + ) + var existingConfigFile: ExistingConfigFileMode = .overwrite + enum ExistingConfigFileMode: String, ExpressibleByArgument, Codable { + case overwrite + case amend + } + + @Option(help: "The name of the Swift module into which the resulting Swift types will be generated.") + var swiftModule: String + + var effectiveSwiftModule: String { + swiftModule + } + + @Option(help: "A prefix that will be added to the names of the Swift types") + var swiftTypePrefix: String? + } +} + +extension SwiftJava.ConfigureCommand { + mutating func runSwiftJavaCommand(config: inout Configuration) async throws { + let classpathEntries = self.configureCommandJVMClasspath( + searchDirs: [self.effectiveSwiftModuleURL], config: config, log: Self.log) + + let jvm = + try self.makeJVM(classpathEntries: classpathEntries) + + try emitConfiguration(classpathEntries: classpathEntries, environment: jvm.environment()) + } + + /// Get base configuration, depending on if we are to 'amend' or 'overwrite' the existing configuration. + func getBaseConfigurationForWrite() throws -> (Bool, Configuration) { + guard let actualOutputDirectory = self.actualOutputDirectory else { + // If output has no path there's nothing to amend + return (false, .init()) + } + + switch self.existingConfigFile { + case .overwrite: + // always make up a fresh instance if we're overwriting + return (false, .init()) + case .amend: + let configPath = actualOutputDirectory + guard let config = try readConfiguration(sourceDir: configPath.path) else { + return (false, .init()) + } + return (true, config) + } + } + + // TODO: make this perhaps "emit type mappings" + mutating func emitConfiguration( + classpathEntries: [String], + environment: JNIEnvironment + ) throws { + var log = Self.log + log.logLevel = .init(rawValue: self.logLevel.rawValue)! + + log.info("Run: emit configuration...") + var (amendExistingConfig, config) = try getBaseConfigurationForWrite() + + if !self.commonOptions.filterInclude.isEmpty { + log.debug("Generate Java->Swift type mappings. Active include filters: \(self.commonOptions.filterInclude)") + } else if let filters = config.filterInclude, !filters.isEmpty { + // take the package filter from the configuration file + self.commonOptions.filterInclude = filters + } else { + log.debug("Generate Java->Swift type mappings. No package include filter applied.") + } + log.debug("Classpath: \(classpathEntries)") + + if classpathEntries.isEmpty { + log.warning("Classpath is empty!") + } + + // Get a fresh or existing configuration we'll amend + if amendExistingConfig { + log.info("Amend existing swift-java.config file...") + } + config.classpath = classpathEntries.joined(separator: ":") // TODO: is this correct? + + // Import types from all the classpath entries; + // Note that we use the package level filtering, so users have some control over what gets imported. + for entry in classpathEntries { + guard fileOrDirectoryExists(at: entry) else { + // We only log specific jars missing, as paths may be empty directories that won't hurt not existing. + log.debug("Classpath entry does not exist: \(entry)") + continue + } + + print("[debug][swift-java] Importing classpath entry: \(entry)") + if entry.hasSuffix(".jar") { + print("[debug][swift-java] Importing classpath as JAR file: \(entry)") + let jarFile = try JarFile(entry, false, environment: environment) + try addJavaToSwiftMappings( + to: &config, + forJar: jarFile, + environment: environment + ) + } else if FileManager.default.fileExists(atPath: entry), let entryURL = URL(string: entry) { + print("[debug][swift-java] Importing classpath as directory: \(entryURL)") + try addJavaToSwiftMappings( + to: &config, + forDirectory: entryURL + ) + } else { + log.warning("Classpath entry does not exist, skipping: \(entry)") + } + } + + // Encode the configuration. + let contents = try config.renderJSON() + + // Write the file. + try writeContents( + contents, + outputDirectory: self.actualOutputDirectory, + to: "swift-java.config", + description: "swift-java configuration file" + ) + } + + mutating func addJavaToSwiftMappings( + to configuration: inout Configuration, + forDirectory url: Foundation.URL + ) throws { + let enumerator = FileManager.default.enumerator(atPath: url.path()) + + while let filePath = enumerator?.nextObject() as? String { + try addJavaToSwiftMappings(to: &configuration, fileName: filePath) + } + } + + mutating func addJavaToSwiftMappings( + to configuration: inout Configuration, + forJar jarFile: JarFile, + environment: JNIEnvironment + ) throws { + for entry in jarFile.entries()! { + try addJavaToSwiftMappings(to: &configuration, fileName: entry.getName()) + } + } + + mutating func addJavaToSwiftMappings( + to configuration: inout Configuration, + fileName: String + ) throws { + // We only look at class files + guard fileName.hasSuffix(".class") else { + return + } + + // Skip some "common" files we know that would be duplicated in every jar + guard !fileName.hasPrefix("META-INF") else { + return + } + guard !fileName.hasSuffix("package-info") else { + return + } + guard !fileName.hasSuffix("package-info.class") else { + return + } + + // If this is a local class, it cannot be mapped into Swift. + if fileName.isLocalJavaClass { + return + } + + let javaCanonicalName = String(fileName.replacing("/", with: ".") + .dropLast(".class".count)) + + guard SwiftJava.shouldImport(javaCanonicalName: javaCanonicalName, commonOptions: self.commonOptions) else { + log.info("Skip importing class: \(javaCanonicalName) due to include/exclude filters") + return + } + + if configuration.classes?[javaCanonicalName] != nil { + // We never overwrite an existing class mapping configuration. + // E.g. the user may have configured a custom name for a type. + return + } + + if configuration.classes == nil { + configuration.classes = [:] + } + + var swiftName = javaCanonicalName.defaultSwiftNameForJavaClass + if let swiftTypePrefix { + swiftName = "\(swiftTypePrefix)\(swiftName)" + } + + if let configuredSwiftName = configuration.classes![javaCanonicalName] { + log.info("Java type '\(javaCanonicalName)' already configured as '\(configuredSwiftName)' Swift type.") + } else { + log.info("Configure Java type '\(javaCanonicalName)' as '\(swiftName.bold)' Swift type.") + } + + configuration.classes![javaCanonicalName] = swiftName + } +} + +package func fileOrDirectoryExists(at path: String) -> Bool { + var isDirectory: ObjCBool = false + return FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory) +} diff --git a/Sources/SwiftJavaTool/Commands/JExtractCommand.swift b/Sources/SwiftJavaTool/Commands/JExtractCommand.swift new file mode 100644 index 000000000..775ab6040 --- /dev/null +++ b/Sources/SwiftJavaTool/Commands/JExtractCommand.swift @@ -0,0 +1,181 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation +import ArgumentParser +import SwiftJavaToolLib +import SwiftJava +import JavaUtilJar +import SwiftJavaToolLib +import JExtractSwiftLib +import SwiftJavaConfigurationShared + +/// Extract Java bindings from Swift sources or interface files. +/// +/// Example usage: +/// ``` +/// > swift-java jextract +// --input-swift Sources/SwiftyBusiness \ +/// --output-swift .build/.../outputs/SwiftyBusiness \ +/// --output-Java .build/.../outputs/Java +/// ``` +extension SwiftJava { + + struct JExtractCommand: SwiftJavaBaseAsyncParsableCommand, HasCommonOptions { + static let configuration = CommandConfiguration( + commandName: "jextract", // TODO: wrap-swift? + abstract: "Wrap Swift functions and types with Java bindings, making them available to be called from Java") + + @OptionGroup var commonOptions: SwiftJava.CommonOptions + + @Option(help: "The mode of generation to use for the output files. Used with jextract mode.") + var mode: JExtractGenerationMode? + + @Option(help: "The name of the Swift module into which the resulting Swift types will be generated.") + var swiftModule: String + + var effectiveSwiftModule: String { + swiftModule + } + + @Option(help: "The Java package the generated Java code should be emitted into.") + var javaPackage: String? = nil + + @Option(help: "The directory where generated Swift files should be written. Generally used with jextract mode.") + var outputSwift: String + + @Option(help: "The directory where generated Java files should be written. Generally used with jextract mode.") + var outputJava: String + + @Flag(help: "Some build systems require an output to be present when it was 'expected', even if empty. This is used by the JExtractSwiftPlugin build plugin, but otherwise should not be necessary.") + var writeEmptyFiles: Bool = false + + @Option(help: "The mode of generation to use for the output files. Used with jextract mode. By default, unsigned Swift types are imported as their bit-width compatible signed Java counterparts, and annotated using the '@Unsigned' annotation. You may choose the 'wrapGuava' mode in order to import types as class wrapper types (`UnsignedInteger` et al) defined by the Google Guava library's `com.google.common.primitives' package. that ensure complete type-safety with regards to unsigned values, however they incur an allocation and performance overhead.") + var unsignedNumbersMode: JExtractUnsignedIntegerMode? + + @Option(help: "The lowest access level of Swift declarations that should be extracted, defaults to 'public'.") + var minimumInputAccessLevelMode: JExtractMinimumAccessLevelMode? + + @Option(help: "The memory management mode to use for the generated code. By default, the user must explicitly provide `SwiftArena` to all calls that require it. By choosing `allowGlobalAutomatic`, user can omit this parameter and a global GC-based arena will be used.") + var memoryManagementMode: JExtractMemoryManagementMode? + + @Option( + help: """ + A swift-java configuration file for a given Swift module name on which this module depends, + e.g., Sources/JavaJar/swift-java.config. There should be one of these options + for each Swift module that this module depends on (transitively) that contains wrapped Java sources. + """ + ) + var dependsOn: [String] = [] + + @Option(help: "The mode to use for extracting asynchronous Swift functions. By default async methods are extracted as Java functions returning CompletableFuture.") + var asyncFuncMode: JExtractAsyncFuncMode? + + @Flag(help: "By enabling this mode, JExtract will generate Java code that allows you to implement Swift protocols using Java classes. This feature requires disabling the sandbox mode in SwiftPM. This only works in the 'jni' mode.") + var enableJavaCallbacks: Bool = false + + @Option(help: "If specified, JExtract will output to this file a list of paths to all generated Java source files") + var generatedJavaSourcesListFileOutput: String? + } +} + +extension SwiftJava.JExtractCommand { + func runSwiftJavaCommand(config: inout Configuration) async throws { + configure(&config.javaPackage, overrideWith: self.javaPackage) + configure(&config.mode, overrideWith: self.mode) + config.swiftModule = self.effectiveSwiftModule + config.outputJavaDirectory = outputJava + config.outputSwiftDirectory = outputSwift + + // @Flag does not support optional, so we check ourself if it is passed + let writeEmptyFiles = CommandLine.arguments.contains("--write-empty-files") ? true : nil + configure(&config.writeEmptyFiles, overrideWith: writeEmptyFiles) + + let enableJavaCallbacks = CommandLine.arguments.contains("--enable-java-callbacks") ? true : nil + configure(&config.enableJavaCallbacks, overrideWith: enableJavaCallbacks) + + configure(&config.unsignedNumbersMode, overrideWith: self.unsignedNumbersMode) + configure(&config.minimumInputAccessLevelMode, overrideWith: self.minimumInputAccessLevelMode) + configure(&config.memoryManagementMode, overrideWith: self.memoryManagementMode) + configure(&config.asyncFuncMode, overrideWith: self.asyncFuncMode) + configure(&config.generatedJavaSourcesListFileOutput, overrideWith: self.generatedJavaSourcesListFileOutput) + + try checkModeCompatibility(config: config) + + if let inputSwift = commonOptions.inputSwift { + config.inputSwiftDirectory = inputSwift + } else if let swiftModule = config.swiftModule { + // This is a "good guess" technically a target can be somewhere else, but then you can use --input-swift + config.inputSwiftDirectory = "\(FileManager.default.currentDirectoryPath)/Sources/\(swiftModule)" + } + + print("[debug][swift-java] Running 'swift-java jextract' in mode: " + "\(config.effectiveMode)".bold) + + // Load all of the dependent configurations and associate them with Swift modules. + let dependentConfigs = try loadDependentConfigs(dependsOn: self.dependsOn) + print("[debug][swift-java] Dependent configs: \(dependentConfigs.count)") + + try jextractSwift(config: config, dependentConfigs: dependentConfigs.map(\.1)) + } + + /// Check if the configured modes are compatible, and fail if not + func checkModeCompatibility(config: Configuration) throws { + if config.effectiveMode == .jni { + switch config.effectiveUnsignedNumbersMode { + case .annotate: + () // OK + case .wrapGuava: + throw IllegalModeCombinationError("JNI mode does not support '\(JExtractUnsignedIntegerMode.wrapGuava)' Unsigned integer mode! \(Self.helpMessage())") + } + } else if config.effectiveMode == .ffm { + guard config.effectiveMemoryManagementMode == .explicit else { + throw IllegalModeCombinationError("FFM mode does not support '\(self.memoryManagementMode ?? .default)' memory management mode! \(Self.helpMessage())") + } + + if let enableJavaCallbacks = config.enableJavaCallbacks, enableJavaCallbacks { + throw IllegalModeCombinationError("FFM mode does not support enabling Java callbacks! \(Self.helpMessage())") + } + } + } +} + +struct IncompatibleModeError: Error { + let message: String + init(_ message: String) { + self.message = message + } +} + +extension SwiftJava.JExtractCommand { + func jextractSwift( + config: Configuration, + dependentConfigs: [Configuration] + ) throws { + try SwiftToJava(config: config, dependentConfigs: dependentConfigs).run() + } + +} + +struct IllegalModeCombinationError: Error { + let message: String + init(_ message: String) { + self.message = message + } +} + +extension JExtractGenerationMode: ExpressibleByArgument {} +extension JExtractUnsignedIntegerMode: ExpressibleByArgument {} +extension JExtractMinimumAccessLevelMode: ExpressibleByArgument {} +extension JExtractMemoryManagementMode: ExpressibleByArgument {} +extension JExtractAsyncFuncMode: ExpressibleByArgument {} diff --git a/Sources/SwiftJavaTool/Commands/ResolveCommand.swift b/Sources/SwiftJavaTool/Commands/ResolveCommand.swift new file mode 100644 index 000000000..7cc4c477d --- /dev/null +++ b/Sources/SwiftJavaTool/Commands/ResolveCommand.swift @@ -0,0 +1,322 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024-2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import ArgumentParser +import Foundation +import SwiftJavaToolLib +import SwiftJava +import Foundation +import JavaUtilJar +import SwiftJavaToolLib +import SwiftJavaConfigurationShared +import SwiftJavaShared +import _Subprocess +#if canImport(System) +import System +#else +@preconcurrency import SystemPackage +#endif + +typealias Configuration = SwiftJavaConfigurationShared.Configuration + +extension SwiftJava { + struct ResolveCommand: SwiftJavaBaseAsyncParsableCommand, HasCommonOptions, HasCommonJVMOptions { + static let configuration = CommandConfiguration( + commandName: "resolve", + abstract: "Resolve dependencies and write the resulting swift-java.classpath file") + + @OptionGroup var commonOptions: SwiftJava.CommonOptions + @OptionGroup var commonJVMOptions: SwiftJava.CommonJVMOptions + + @Option(help: "The name of the Swift module into which the resulting Swift types will be generated.") + var swiftModule: String + + var effectiveSwiftModule: String { + swiftModule + } + + @Argument( + help: """ + Additional configuration paths (swift-java.config) files, with defined 'dependencies', \ + or dependency descriptors formatted as 'groupID:artifactID:version' separated by ','. \ + May be empty, in which case the target Swift module's configuration's 'dependencies' will be used. + """ + ) + var input: String? + } +} + +extension SwiftJava.ResolveCommand { + var SwiftJavaClasspathPrefix: String { "SWIFT_JAVA_CLASSPATH:" } + var printRuntimeClasspathTaskName: String { "printRuntimeClasspath" } + + mutating func runSwiftJavaCommand(config: inout Configuration) async throws { + var dependenciesToResolve: [JavaDependencyDescriptor] = [] + if let input, let inputDependencies = parseDependencyDescriptor(input) { + dependenciesToResolve.append(inputDependencies) + } + if let dependencies = config.dependencies { + dependenciesToResolve += dependencies + } + + if dependenciesToResolve.isEmpty { + print("[warn][swift-java] Attempted to 'resolve' dependencies but no dependencies specified in swift-java.config or command input!") + return + } + + let dependenciesClasspath = + try await resolveDependencies(swiftModule: swiftModule, dependencies: dependenciesToResolve) + + // FIXME: disentangle the output directory from SwiftJava and then make it a required option in this Command + guard let outputDirectory = self.commonOptions.outputDirectory else { + fatalError("error: Must specify --output-directory in 'resolve' mode! This option will become explicitly required") + } + + try writeSwiftJavaClasspathFile( + swiftModule: swiftModule, + outputDirectory: outputDirectory, + resolvedClasspath: dependenciesClasspath) + } + + + /// Resolves Java dependencies from swift-java.config and returns classpath information. + /// + /// - Parameters: + /// - swiftModule: module name from --swift-module. e.g.: --swift-module MySwiftModule + /// - dependencies: parsed maven-style dependency descriptors (groupId:artifactId:version) + /// from Sources/MySwiftModule/swift-java.config "dependencies" array. + /// + /// - Throws: + func resolveDependencies( + swiftModule: String, dependencies: [JavaDependencyDescriptor] + ) async throws -> ResolvedDependencyClasspath { + let deps = dependencies.map { $0.descriptionGradleStyle } + print("[debug][swift-java] Resolve and fetch dependencies for: \(deps)") + + let workDir = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) + .appendingPathComponent(".build") + + let dependenciesClasspath = await resolveDependencies(workDir: workDir, dependencies: dependencies) + let classpathEntries = dependenciesClasspath.split(separator: ":") + + print("[info][swift-java] Resolved classpath for \(deps.count) dependencies of '\(swiftModule)', classpath entries: \(classpathEntries.count), ", terminator: "") + print("done.".green) + + for entry in classpathEntries { + print("[info][swift-java] Classpath entry: \(entry)") + } + + return ResolvedDependencyClasspath(for: dependencies, classpath: dependenciesClasspath) + } + + + /// Resolves maven-style dependencies from swift-java.config under temporary project directory. + /// + /// - Parameter dependencies: maven-style dependencies to resolve + /// - Returns: Colon-separated classpath + func resolveDependencies(workDir: URL, dependencies: [JavaDependencyDescriptor]) async -> String { + print("Create directory: \(workDir.absoluteString)") + + let resolverDir: URL + do { + resolverDir = try createTemporaryDirectory(in: workDir) + } catch { + fatalError("Unable to create temp directory at: \(workDir.absoluteString)! \(error)") + } + defer { + try? FileManager.default.removeItem(at: resolverDir) + } + + // We try! because it's easier to track down errors like this than when we bubble up the errors, + // and don't get great diagnostics or backtraces due to how swiftpm plugin tools are executed. + + try! copyGradlew(to: resolverDir) + + try! printGradleProject(directory: resolverDir, dependencies: dependencies) + + if #available(macOS 15, *) { + let process = try! await _Subprocess.run( + .path(FilePath(resolverDir.appendingPathComponent("gradlew").path)), + arguments: [ + "--no-daemon", + "--rerun-tasks", + "\(printRuntimeClasspathTaskName)", + ], + workingDirectory: Optional(FilePath(resolverDir.path)), + // TODO: we could move to stream processing the outputs + output: .string(limit: Int.max, encoding: UTF8.self), // Don't limit output, we know it will be reasonable size + error: .string(limit: Int.max, encoding: UTF8.self) // Don't limit output, we know it will be reasonable size + ) + + let outString = process.standardOutput ?? "" + let errString = process.standardError ?? "" + + let classpathOutput: String + if let found = outString.split(separator: "\n").first(where: { $0.hasPrefix(self.SwiftJavaClasspathPrefix) }) { + classpathOutput = String(found) + } else if let found = errString.split(separator: "\n").first(where: { $0.hasPrefix(self.SwiftJavaClasspathPrefix) }) { + classpathOutput = String(found) + } else { + let suggestDisablingSandbox = "It may be that the Sandbox has prevented dependency fetching, please re-run with '--disable-sandbox'." + fatalError("Gradle output had no SWIFT_JAVA_CLASSPATH! \(suggestDisablingSandbox). \n" + + "Command was: \(CommandLine.arguments.joined(separator: " ").bold)\n" + + "Output was: <<<\(outString)>>>;\n" + + "Err was: <<<\(errString)>>>") + } + + return String(classpathOutput.dropFirst(SwiftJavaClasspathPrefix.count)) + } else { + // Subprocess is unavailable + fatalError("Subprocess is unavailable yet required to execute `gradlew` subprocess. Please update to macOS 15+") + } + } + + /// Creates Gradle project files (build.gradle, settings.gradle.kts) in temporary directory. + func printGradleProject(directory: URL, dependencies: [JavaDependencyDescriptor]) throws { + let buildGradle = directory + .appendingPathComponent("build.gradle", isDirectory: false) + + let buildGradleText = + """ + plugins { id 'java-library' } + repositories { mavenCentral() } + + dependencies { + \(dependencies.map({ dep in "implementation(\"\(dep.descriptionGradleStyle)\")" }).joined(separator: ",\n")) + } + + tasks.register("printRuntimeClasspath") { + def runtimeClasspath = sourceSets.main.runtimeClasspath + inputs.files(runtimeClasspath) + doLast { + println("\(SwiftJavaClasspathPrefix)${runtimeClasspath.asPath}") + } + } + """ + try buildGradleText.write(to: buildGradle, atomically: true, encoding: .utf8) + + let settingsGradle = directory + .appendingPathComponent("settings.gradle.kts", isDirectory: false) + let settingsGradleText = + """ + rootProject.name = "swift-java-resolve-temp-project" + """ + try settingsGradleText.write(to: settingsGradle, atomically: true, encoding: .utf8) + } + + /// Creates {MySwiftModule}.swift.classpath in the --output-directory. + /// + /// - Parameters: + /// - swiftModule: Swift module name for classpath filename (--swift-module value) + /// - outputDirectory: Directory path for classpath file (--output-directory value) + /// - resolvedClasspath: Complete dependency classpath information + /// + mutating func writeSwiftJavaClasspathFile( + swiftModule: String, + outputDirectory: String, + resolvedClasspath: ResolvedDependencyClasspath) throws { + // Convert the artifact name to a module name + // e.g. reactive-streams -> ReactiveStreams + + // The file contents are just plain + let contents = resolvedClasspath.classpath + + let filename = "\(swiftModule).swift-java.classpath" + print("[debug][swift-java] Write resolved dependencies to: \(outputDirectory)/\(filename)") + + // Write the file + try writeContents( + contents, + outputDirectory: URL(fileURLWithPath: outputDirectory), + to: filename, + description: "swift-java.classpath file for module \(swiftModule)" + ) + } + + public func artifactIDAsModuleID(_ artifactID: String) -> String { + let components = artifactID.split(whereSeparator: { $0 == "-" }) + let camelCased = components.map { $0.capitalized }.joined() + return camelCased + } + + // copy gradlew & gradle.bat from root, throws error if there is no gradle setup. + func copyGradlew(to resolverWorkDirectory: URL) throws { + var searchDir = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) + + while searchDir.pathComponents.count > 1 { + let gradlewFile = searchDir.appendingPathComponent("gradlew") + let gradlewExists = FileManager.default.fileExists(atPath: gradlewFile.path) + guard gradlewExists else { + searchDir = searchDir.deletingLastPathComponent() + continue + } + + let gradlewBatFile = searchDir.appendingPathComponent("gradlew.bat") + let gradlewBatExists = FileManager.default.fileExists(atPath: gradlewFile.path) + + let gradleDir = searchDir.appendingPathComponent("gradle") + let gradleDirExists = FileManager.default.fileExists(atPath: gradleDir.path) + guard gradleDirExists else { + searchDir = searchDir.deletingLastPathComponent() + continue + } + + // TODO: gradle.bat as well + try? FileManager.default.copyItem( + at: gradlewFile, + to: resolverWorkDirectory.appendingPathComponent("gradlew")) + if gradlewBatExists { + try? FileManager.default.copyItem( + at: gradlewBatFile, + to: resolverWorkDirectory.appendingPathComponent("gradlew.bat")) + } + try? FileManager.default.copyItem( + at: gradleDir, + to: resolverWorkDirectory.appendingPathComponent("gradle")) + return + } + } + + func createTemporaryDirectory(in directory: URL) throws -> URL { + let uuid = UUID().uuidString + let resolverDirectoryURL = directory.appendingPathComponent("swift-java-dependencies-\(uuid)") + + try FileManager.default.createDirectory(at: resolverDirectoryURL, withIntermediateDirectories: true, attributes: nil) + + return resolverDirectoryURL + } + +} + +struct ResolvedDependencyClasspath: CustomStringConvertible { + /// The dependency identifiers this is the classpath for. + let rootDependencies: [JavaDependencyDescriptor] + + /// Plain string representation of a Java classpath + let classpath: String + + var classpathEntries: [String] { + classpath.split(separator: ":").map(String.init) + } + + init(for rootDependencies: [JavaDependencyDescriptor], classpath: String) { + self.rootDependencies = rootDependencies + self.classpath = classpath + } + + var description: String { + "JavaClasspath(for: \(rootDependencies), classpath: \(classpath))" + } +} + diff --git a/Sources/SwiftJavaTool/Commands/WrapJavaCommand.swift b/Sources/SwiftJavaTool/Commands/WrapJavaCommand.swift new file mode 100644 index 000000000..3e4e482b3 --- /dev/null +++ b/Sources/SwiftJavaTool/Commands/WrapJavaCommand.swift @@ -0,0 +1,365 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024-2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation +import ArgumentParser +import Logging +import SwiftJavaToolLib +import SwiftJava +import JavaUtilJar +import SwiftJavaConfigurationShared + +extension SwiftJava { + + struct WrapJavaCommand: SwiftJavaBaseAsyncParsableCommand, HasCommonOptions, HasCommonJVMOptions { + + static let log: Logging.Logger = .init(label: "swift-java:\(configuration.commandName!)") + + static let configuration = CommandConfiguration( + commandName: "wrap-java", + abstract: "Wrap Java classes with corresponding Swift bindings.") + + @OptionGroup var commonOptions: SwiftJava.CommonOptions + @OptionGroup var commonJVMOptions: SwiftJava.CommonJVMOptions + + @Option(help: "The name of the Swift module into which the resulting Swift types will be generated.") + var swiftModule: String + + var effectiveSwiftModule: String { + swiftModule + } + + @Option( + help: """ + A swift-java configuration file for a given Swift module name on which this module depends, + e.g., JavaKitJar=Sources/JavaKitJar/swift-java.config. There should be one of these options + for each Swift module that this module depends on (transitively) that contains wrapped Java sources. + """ + ) + var dependsOn: [String] = [] + + @Option(help: "The names of Java classes whose declared native methods will be implemented in Swift.") + var swiftNativeImplementation: [String] = [] + + @Option(help: "Cache directory for intermediate results and other outputs between runs") + var cacheDirectory: String? + + @Option(help: "Match java package directory structure with generated Swift files") + var swiftMatchPackageDirectoryStructure: Bool = false + + @Option(help: "If specified, a single Swift file will be generated containing all the generated code") + var singleSwiftFileOutput: String? + } +} + +extension SwiftJava.WrapJavaCommand { + + mutating func runSwiftJavaCommand(config: inout Configuration) async throws { + print("self.commonOptions.filterInclude = \(self.commonOptions.filterInclude)") + configure(&config.filterInclude, append: self.commonOptions.filterInclude) + configure(&config.filterExclude, append: self.commonOptions.filterExclude) + configure(&config.singleSwiftFileOutput, overrideWith: self.singleSwiftFileOutput) + + // Get base classpath configuration for this target and configuration + var classpathSearchDirs = [self.effectiveSwiftModuleURL] + if let cacheDir = self.cacheDirectory { + print("[trace][swift-java] Cache directory: \(cacheDir)") + classpathSearchDirs += [URL(fileURLWithPath: cacheDir)] + } else { + print("[trace][swift-java] Cache directory: none") + } + + var classpathEntries = self.configureCommandJVMClasspath( + searchDirs: classpathSearchDirs, config: config, log: Self.log) + + // Load all of the dependent configurations and associate them with Swift modules. + let dependentConfigs = try loadDependentConfigs(dependsOn: self.dependsOn).map { moduleName, config in + guard let moduleName else { + throw JavaToSwiftError.badConfigOption + } + return (moduleName, config) + } + print("[debug][swift-java] Dependent configs: \(dependentConfigs.count)") + + // Include classpath entries which libs we depend on require... + for (fromModule, config) in dependentConfigs { + print("[trace][swift-java] Add dependent config (\(fromModule)) classpath elements: \(config.classpathEntries.count)") + // TODO: may need to resolve the dependent configs rather than just get their configs + // TODO: We should cache the resolved classpaths as well so we don't do it many times + for entry in config.classpathEntries { + print("[trace][swift-java] Add dependent config (\(fromModule)) classpath element: \(entry)") + classpathEntries.append(entry) + } + } + + let jvm = try self.makeJVM(classpathEntries: classpathEntries) + + try self.generateWrappers( + config: config, + // classpathEntries: classpathEntries, + dependentConfigs: dependentConfigs, + environment: jvm.environment() + ) + } +} + +extension SwiftJava.WrapJavaCommand { + + mutating func generateWrappers( + config: Configuration, + dependentConfigs: [(String, Configuration)], + environment: JNIEnvironment + ) throws { + let translator = JavaTranslator( + config: config, + swiftModuleName: effectiveSwiftModule, + environment: environment, + translateAsClass: true + ) + + log.info("Active include filters: \(config.filterInclude ?? [])") + log.info("Active exclude filters: \(config.filterExclude ?? [])") + + // Keep track of all of the Java classes that will have + // Swift-native implementations. + translator.swiftNativeImplementations = Set(swiftNativeImplementation) + + // Note all of the dependent configurations. + for (swiftModuleName, dependentConfig) in dependentConfigs { + translator.addConfiguration( + dependentConfig, + forSwiftModule: swiftModuleName + ) + } + + // Add the configuration for this module. + translator.addConfiguration(config, forSwiftModule: effectiveSwiftModule) + + // Load all of the explicitly-requested classes. + let classLoader = try! JavaClass(environment: environment) + .getSystemClassLoader()! + var javaClasses: [JavaClass] = [] + for (javaClassName, _) in config.classes ?? [:] { + func remove() { + translator.translatedClasses.removeValue(forKey: javaClassName) + } + + guard shouldImportJavaClass(javaClassName, config: config) else { + remove() + continue + } + + guard let javaClass = try classLoader.loadClass(javaClassName) else { + log.warning("Could not load Java class '\(javaClassName)', skipping.") + remove() + continue + } + + guard self.shouldExtract(javaClass: javaClass, config: config) else { + log.info("Skip Java type: \(javaClassName) (does not match minimum access level)") + remove() + continue + } + + guard !javaClass.isEnum() else { + log.info("Skip Java type: \(javaClassName) (enums do not currently work)") + remove() + continue + } + + log.info("Wrapping java type: \(javaClassName)") + + // Add this class to the list of classes we'll translate. + javaClasses.append(javaClass) + } + + log.info("OK now we go to nested classes") + + // Find all of the nested classes for each class, adding them to the list + // of classes to be translated if they were already specified. + var allClassesToVisit = javaClasses + var currentClassIndex: Int = 0 + outerClassLoop: while currentClassIndex < allClassesToVisit.count { + defer { + currentClassIndex += 1 + } + + // The current top-level class we're in. + let currentClass = allClassesToVisit[currentClassIndex] + let currentClassName = currentClass.getName() + guard let currentSwiftName = translator.translatedClasses[currentClass.getName()]?.swiftType else { + continue + } + + // Find all of the nested classes that weren't explicitly translated already. + let nestedAndSuperclassNestedClasses = currentClass.getClasses() // watch out, this includes nested types from superclasses + let nestedClasses: [JavaClass] = nestedAndSuperclassNestedClasses.compactMap { nestedClass in + guard let nestedClass else { + return nil + } + + // If this is a local class, we're done. + let javaClassName = nestedClass.getName() + if javaClassName.isLocalJavaClass { + return nil + } + + // We only want to visit and import types which are explicitly inside this decl, + // and NOT any of the types contained in the super classes. That would violate our "current class" + // nesting, because those are *actually* nested in the other class, not "the current one" (i.e. in a super class). + guard javaClassName.hasPrefix(currentClassName) else { + log.trace("Skip super-class nested class '\(javaClassName)', is not member of \(currentClassName). Will be visited independently.") + return nil + } + + guard shouldImportJavaClass(javaClassName, config: config) else { + return nil + } + + // If this class has been explicitly mentioned, we're done. + guard translator.translatedClasses[javaClassName] == nil else { + return nil + } + + guard self.shouldExtract(javaClass: nestedClass, config: config) else { + log.info("Skip Java type: \(javaClassName) (does not match minimum access level)") + return nil + } + + guard !nestedClass.isEnum() else { + log.info("Skip Java type: \(javaClassName) (enums do not currently work)") + return nil + } + + // Record this as a translated class. + let swiftUnqualifiedName = javaClassName.javaClassNameToCanonicalName + .defaultSwiftNameForJavaClass + + let swiftName = "\(currentSwiftName).\(swiftUnqualifiedName)" + let translatedSwiftName = SwiftTypeName(module: nil, name: swiftName) + translator.translatedClasses[javaClassName] = translatedSwiftName + log.debug("Record translated Java class '\(javaClassName)' -> \(translatedSwiftName)") + return nestedClass + } + + // If there were no new nested classes, there's nothing to do. + if nestedClasses.isEmpty { + continue + } + + // Record all of the nested classes that we will visit. + translator.nestedClasses[currentClass.getName()] = nestedClasses + allClassesToVisit.append(contentsOf: nestedClasses) + } + + // Validate configurations before writing any files + try translator.validateClassConfiguration() + + // Translate all of the Java classes into Swift classes. + + if let singleSwiftFileOutput = config.singleSwiftFileOutput { + translator.startNewFile() + + let swiftClassDecls = try javaClasses.flatMap { + try translator.translateClass($0) + } + let importDecls = translator.getImportDecls() + + let swiftFileText = """ + // Auto-generated by Java-to-Swift wrapper generator. + \(importDecls.map { $0.description }.joined()) + \(swiftClassDecls.map { $0.description }.joined(separator: "\n")) + + """ + + try writeContents( + swiftFileText, + outputDirectory: self.actualOutputDirectory, + to: singleSwiftFileOutput, + description: "Java class translation" + ) + } else { + for javaClass in javaClasses { + translator.startNewFile() + + let swiftClassDecls = try translator.translateClass(javaClass) + let importDecls = translator.getImportDecls() + + let swiftFileText = """ + // Auto-generated by Java-to-Swift wrapper generator. + \(importDecls.map { $0.description }.joined()) + \(swiftClassDecls.map { $0.description }.joined(separator: "\n")) + + """ + + var generatedFileOutputDir = self.actualOutputDirectory + if self.swiftMatchPackageDirectoryStructure { + generatedFileOutputDir?.append(path: javaClass.getPackageName().replacing(".", with: "/")) + } + + let swiftFileName = try translator.getSwiftTypeName(javaClass, preferValueTypes: false) + .swiftName.replacing(".", with: "+") + ".swift" + try writeContents( + swiftFileText, + outputDirectory: generatedFileOutputDir, + to: swiftFileName, + description: "Java class '\(javaClass.getName())' translation" + ) + } + } + } + + /// Determines whether a method should be extracted for translation. + /// Only look at public and protected methods here. + private func shouldExtract(javaClass: JavaClass, config: Configuration) -> Bool { + switch config.effectiveMinimumInputAccessLevelMode { + case .internal: + return javaClass.isPublic || javaClass.isProtected || javaClass.isPackage + case .package: + return javaClass.isPublic || javaClass.isProtected || javaClass.isPackage + case .public: + return javaClass.isPublic || javaClass.isProtected + } + } + + private func shouldImportJavaClass(_ javaClassName: String, config: Configuration) -> Bool { + // If we have an inclusive filter, import only types from it + if let includes = config.filterInclude, !includes.isEmpty { + let anyIncludeFilterMatched = includes.contains { include in + if javaClassName.starts(with: include) { + // TODO: lower to trace level + log.info("Skip Java type: \(javaClassName) (does not match any include filter)") + return true + } + + return false + } + + guard anyIncludeFilterMatched else { + log.info("Skip Java type: \(javaClassName) (does not match any include filter)") + return false + } + } + // If we have an exclude filter, check for it as well + for exclude in config.filterExclude ?? [] { + if javaClassName.starts(with: exclude) { + log.info("Skip Java type: \(javaClassName) (does match exclude filter: \(exclude))") + return false + } + } + + // The class matches import filters, if any, and was not excluded. + return true + } +} diff --git a/Sources/SwiftJavaTool/CommonOptions.swift b/Sources/SwiftJavaTool/CommonOptions.swift new file mode 100644 index 000000000..4627381e0 --- /dev/null +++ b/Sources/SwiftJavaTool/CommonOptions.swift @@ -0,0 +1,151 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import ArgumentParser +import Foundation +import SwiftJavaToolLib +import JExtractSwiftLib +import SwiftJava +import JavaUtilJar +import JavaNet +import SwiftSyntax +import Logging +import SwiftJavaConfigurationShared +import SwiftJavaShared + +// - MARK: Common Options + +protocol HasCommonOptions { + var commonOptions: SwiftJava.CommonOptions { get set } +} +extension HasCommonOptions { + func configure(_ setting: inout T?, overrideWith value: T?) { + if let value { + setting = value + } + } + + func configure(_ setting: inout [T]?, append value: [T]?) { + if let value { + if setting == nil { + setting = value + } else { + setting?.append(contentsOf: value) + } + } + } + + var outputDirectory: String? { + self.commonOptions.outputDirectory + } +} + +extension SwiftJava { + struct CommonOptions: ParsableArguments { + @Option(name: .shortAndLong, help: "The directory in which to output generated SwiftJava configuration files.") + var outputDirectory: String? = nil + + @Option(help: "Directory containing Swift files which should be extracted into Java bindings. Also known as 'jextract' mode. Must be paired with --output-java and --output-swift.") + var inputSwift: String? = nil + + @Option(name: .shortAndLong, help: "Configure the level of logs that should be printed") + var logLevel: JExtractSwiftLib.Logger.Level = .info + + @Option(name: .long, help: "While scanning a classpath, inspect ONLY types included in these packages") + var filterInclude: [String] = [] + + @Option(name: .long, help: "While scanning a classpath, skip types which match the filter prefix") + var filterExclude: [String] = [] + + @Option(help: "A path to a custom swift-java.config to use") + var config: String? = nil + } + + struct CommonJVMOptions: ParsableArguments { + @Option( + name: [.customLong("cp"), .customLong("classpath")], + help: "Class search path of directories and zip/jar files from which Java classes can be loaded." + ) + var classpath: [String] = [] + } +} + +// - MARK: Common JVM Options + +protocol HasCommonJVMOptions { + var commonJVMOptions: SwiftJava.CommonJVMOptions { get set } +} +extension HasCommonJVMOptions { + var classpathEntries: [String] { + self.commonJVMOptions.classpath.flatMap { $0.split(separator: ":").map(String.init) } + } + var classpathEnvEntries: [String] { + ProcessInfo.processInfo.environment["CLASSPATH"]?.split(separator: ":").map(String.init) ?? [] + } +} + +extension HasCommonJVMOptions { + + /// Collect classpath information from various sources such as CLASSPATH, `-cp` option and + /// swift-java.classpath files as configured. + /// Parameters: + /// - searchDirs: search directories where we can find swift.java.classpath files to include in the configuration + func configureCommandJVMClasspath(searchDirs: [Foundation.URL], config: Configuration, log: Logging.Logger) -> [String] { + // Form a class path from all of our input sources: + // * Command-line option --classpath + let classpathOptionEntries: [String] = self.classpathEntries + let classpathFromEnv = ProcessInfo.processInfo.environment["CLASSPATH"]?.split(separator: ":").map(String.init) ?? [] + log.debug("Base classpath from CLASSPATH environment: \(classpathFromEnv)") + let classpathFromConfig: [String] = config.classpath?.split(separator: ":").map(String.init) ?? [] + log.debug("Base classpath from config: \(classpathFromConfig)") + + var classpathEntries: [String] = classpathFromConfig + + for searchDir in searchDirs { + let classPathFilesSearchDirectory = searchDir.path + log.debug("Search *.swift-java.classpath in: \(classPathFilesSearchDirectory)") + let foundSwiftJavaClasspath = findSwiftJavaClasspaths(in: classPathFilesSearchDirectory) + + log.debug("Classpath from *.swift-java.classpath files: \(foundSwiftJavaClasspath)") + classpathEntries += foundSwiftJavaClasspath + } + + if !classpathOptionEntries.isEmpty { + log.debug("Classpath from options: \(classpathOptionEntries)") + classpathEntries += classpathOptionEntries + } else { + // * Base classpath from CLASSPATH env variable + log.debug("Classpath from environment: \(classpathFromEnv)") + classpathEntries += classpathFromEnv + } + + let extraClasspath = self.commonJVMOptions.classpath + let extraClasspathEntries = extraClasspath.split(separator: ":").map(String.init) + log.debug("Extra classpath: \(extraClasspathEntries)") + classpathEntries += extraClasspathEntries + + // Bring up the Java VM when necessary + + if log.logLevel >= .debug { + let classpathString = classpathEntries.joined(separator: ":") + log.debug("Initialize JVM with classpath: \(classpathString)") + } + + return classpathEntries + } + + func makeJVM(classpathEntries: [String]) throws -> JavaVirtualMachine { + try JavaVirtualMachine.shared(classpath: classpathEntries) + } +} diff --git a/Sources/SwiftJavaTool/ExcludedJDKTypes.swift b/Sources/SwiftJavaTool/ExcludedJDKTypes.swift new file mode 100644 index 000000000..1d24022f5 --- /dev/null +++ b/Sources/SwiftJavaTool/ExcludedJDKTypes.swift @@ -0,0 +1,42 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +extension SwiftJava { + /// Some types we cannot handle importing, so we hardcode skipping them. + public static let ExcludedJDKTypes: Set = [ + "java.lang.Enum", + "java.lang.Enum$EnumDesc", + ] + + static func shouldImport(javaCanonicalName: String, commonOptions: SwiftJava.CommonOptions) -> Bool { + if SwiftJava.ExcludedJDKTypes.contains(javaCanonicalName) { + return false + } + + for include in commonOptions.filterInclude { + guard javaCanonicalName.hasPrefix(include) else { + // Skip classes which don't match our expected prefix + return false + } + } + + for exclude in commonOptions.filterExclude { + if javaCanonicalName.hasPrefix(exclude) { + return false + } + } + + return true + } +} \ No newline at end of file diff --git a/Sources/SwiftJavaTool/Java/JavaClassLoader.swift b/Sources/SwiftJavaTool/Java/JavaClassLoader.swift new file mode 100644 index 000000000..276508855 --- /dev/null +++ b/Sources/SwiftJavaTool/Java/JavaClassLoader.swift @@ -0,0 +1,30 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftJavaToolLib +import SwiftJavaShared +import CSwiftJavaJNI +import SwiftJava + +// FIXME: do we need this here or can we rely on the generated one? +@JavaClass("java.lang.ClassLoader") +public struct ClassLoader { + @JavaMethod + public func loadClass(_ arg0: String) throws -> JavaClass? +} + +extension JavaClass { + @JavaStaticMethod + public func getSystemClassLoader() -> ClassLoader? +} \ No newline at end of file diff --git a/Sources/SwiftJavaTool/SwiftJava+EmitConfiguration.swift b/Sources/SwiftJavaTool/SwiftJava+EmitConfiguration.swift deleted file mode 100644 index e029d2db6..000000000 --- a/Sources/SwiftJavaTool/SwiftJava+EmitConfiguration.swift +++ /dev/null @@ -1,120 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift.org project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift.org project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import Foundation -import ArgumentParser -import SwiftJavaLib -import JavaKit -import JavaKitJar -import JavaKitConfigurationShared - -extension SwiftJava { - - // TODO: make this perhaps "emit type mappings" - mutating func emitConfiguration( - classpath: String, - environment: JNIEnvironment - ) throws { - print("[java-swift] Generate Java->Swift type mappings. Active filter: \(javaPackageFilter)") - print("[java-swift] Classpath: \(classpath)") - - if classpath.isEmpty { - print("[warning][java-swift] Classpath is empty!") - } - - // Get a fresh or existing configuration we'll amend - var (amendExistingConfig, configuration) = try getBaseConfigurationForWrite() - if amendExistingConfig { - print("[swift-java] Amend existing swift-java.config file...") - } - configuration.classpath = classpath // TODO: is this correct? - - // Import types from all the classpath entries; - // Note that we use the package level filtering, so users have some control over what gets imported. - for entry in classpath.split(separator: ":").map(String.init) { - print("[debug][swift-java] Importing classpath entry: \(entry)") - if entry.hasSuffix(".jar") { - let jarFile = try JarFile(entry, false, environment: environment) - try addJavaToSwiftMappings( - to: &configuration, - forJar: jarFile, - environment: environment - ) - } else if FileManager.default.fileExists(atPath: entry) { - print("[warning][swift-java] Currently unable handle directory classpath entries for config generation! Skipping: \(entry)") - } else { - print("[warning][swift-java] Classpath entry does not exist, skipping: \(entry)") - } - } - - // Encode the configuration. - let contents = try configuration.renderJSON() - - // Write the file. - try writeContents( - contents, - to: "swift-java.config", - description: "swift-java configuration file" - ) - } - - mutating func addJavaToSwiftMappings( - to configuration: inout Configuration, - forJar jarFile: JarFile, - environment: JNIEnvironment - ) throws { - for entry in jarFile.entries()! { - // We only look at class files in the Jar file. - guard entry.getName().hasSuffix(".class") else { - continue - } - - // Skip some "common" files we know that would be duplicated in every jar - guard !entry.getName().hasPrefix("META-INF") else { - continue - } - guard !entry.getName().hasSuffix("package-info") else { - continue - } - guard !entry.getName().hasSuffix("package-info.class") else { - continue - } - - // If this is a local class, it cannot be mapped into Swift. - if entry.getName().isLocalJavaClass { - continue - } - - let javaCanonicalName = String(entry.getName().replacing("/", with: ".") - .dropLast(".class".count)) - - if let javaPackageFilter { - if !javaCanonicalName.hasPrefix(javaPackageFilter) { - // Skip classes which don't match our expected prefix - continue - } - } - - if configuration.classes?[javaCanonicalName] != nil { - // We never overwrite an existing class mapping configuration. - // E.g. the user may have configured a custom name for a type. - continue - } - - configuration.classes?[javaCanonicalName] = - javaCanonicalName.defaultSwiftNameForJavaClass - } - } - -} \ No newline at end of file diff --git a/Sources/SwiftJavaTool/SwiftJava+FetchDependencies.swift b/Sources/SwiftJavaTool/SwiftJava+FetchDependencies.swift deleted file mode 100644 index 47570b199..000000000 --- a/Sources/SwiftJavaTool/SwiftJava+FetchDependencies.swift +++ /dev/null @@ -1,227 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift.org project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift.org project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import Foundation -import SwiftJavaLib -import JavaKit -import Foundation -import JavaKitJar -import SwiftJavaLib -import JavaKitConfigurationShared -import JavaKitShared -import _Subprocess - -extension SwiftJava { - - var SwiftJavaClasspathPrefix: String { "SWIFT_JAVA_CLASSPATH:" } - - var printRuntimeClasspathTaskName: String { "printRuntimeClasspath" } - - func fetchDependencies(moduleName: String, - dependencies: [JavaDependencyDescriptor]) async throws -> ResolvedDependencyClasspath { - let deps = dependencies.map { $0.descriptionGradleStyle } - print("[debug][swift-java] Resolve and fetch dependencies for: \(deps)") - - let dependenciesClasspath = await resolveDependencies(dependencies: dependencies) - let classpathEntries = dependenciesClasspath.split(separator: ":") - - - print("[info][swift-java] Resolved classpath for \(deps.count) dependencies of '\(moduleName)', classpath entries: \(classpathEntries.count), ", terminator: "") - print("done.".green) - - for entry in classpathEntries { - print("[info][swift-java] Classpath entry: \(entry)") - } - - return ResolvedDependencyClasspath(for: dependencies, classpath: dependenciesClasspath) - } - - func resolveDependencies(dependencies: [JavaDependencyDescriptor]) async -> String { - let workDir = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) - .appendingPathComponent(".build") - let resolverDir = try! createTemporaryDirectory(in: workDir) - defer { - try? FileManager.default.removeItem(at: resolverDir) - } - - // We try! because it's easier to track down errors like this than when we bubble up the errors, - // and don't get great diagnostics or backtraces due to how swiftpm plugin tools are executed. - - try! copyGradlew(to: resolverDir) - - try! printGradleProject(directory: resolverDir, dependencies: dependencies) - - let process = try! await Subprocess.run( - .at(.init(resolverDir.appendingPathComponent("gradlew").path)), - arguments: [ - "--no-daemon", - "--rerun-tasks", - "\(printRuntimeClasspathTaskName)", - ], - workingDirectory: .init(platformString: resolverDir.path) - ) - - let outString = String( - data: process.standardOutput, - encoding: .utf8 - ) - let errString = String( - data: process.standardError, - encoding: .utf8 - ) - - let classpathOutput: String - if let found = outString?.split(separator: "\n").first(where: { $0.hasPrefix(self.SwiftJavaClasspathPrefix) }) { - classpathOutput = String(found) - } else if let found = errString?.split(separator: "\n").first(where: { $0.hasPrefix(self.SwiftJavaClasspathPrefix) }) { - classpathOutput = String(found) - } else { - let suggestDisablingSandbox = "It may be that the Sandbox has prevented dependency fetching, please re-run with '--disable-sandbox'." - fatalError("Gradle output had no SWIFT_JAVA_CLASSPATH! \(suggestDisablingSandbox). \n" + - "Output was:<<<\(outString ?? "")>>>; Err was:<<<\(errString ?? "")>>>") - } - - return String(classpathOutput.dropFirst(SwiftJavaClasspathPrefix.count)) - } - - func printGradleProject(directory: URL, dependencies: [JavaDependencyDescriptor]) throws { - let buildGradle = directory - .appendingPathComponent("build.gradle", isDirectory: false) - - let buildGradleText = - """ - plugins { id 'java-library' } - repositories { mavenCentral() } - - dependencies { - \(dependencies.map({ dep in "implementation(\"\(dep.descriptionGradleStyle)\")" }).joined(separator: ",\n")) - } - - tasks.register("printRuntimeClasspath") { - def runtimeClasspath = sourceSets.main.runtimeClasspath - inputs.files(runtimeClasspath) - doLast { - println("\(SwiftJavaClasspathPrefix)${runtimeClasspath.asPath}") - } - } - """ - try buildGradleText.write(to: buildGradle, atomically: true, encoding: .utf8) - - let settingsGradle = directory - .appendingPathComponent("settings.gradle.kts", isDirectory: false) - let settingsGradleText = - """ - rootProject.name = "swift-java-resolve-temp-project" - """ - try settingsGradleText.write(to: settingsGradle, atomically: true, encoding: .utf8) - } - - mutating func writeFetchedDependenciesClasspath( - moduleName: String, - cacheDir: String, - resolvedClasspath: ResolvedDependencyClasspath) throws { - // Convert the artifact name to a module name - // e.g. reactive-streams -> ReactiveStreams - - // The file contents are just plain - let contents = resolvedClasspath.classpath - - print("[debug][swift-java] Resolved dependency: \(classpath)") - - // Write the file - try writeContents( - contents, - outputDirectoryOverride: URL(fileURLWithPath: cacheDir), - to: "\(moduleName).swift-java.classpath", - description: "swift-java.classpath file for module \(moduleName)" - ) - } - - public func artifactIDAsModuleID(_ artifactID: String) -> String { - let components = artifactID.split(whereSeparator: { $0 == "-" }) - let camelCased = components.map { $0.capitalized }.joined() - return camelCased - } - - func copyGradlew(to resolverWorkDirectory: URL) throws { - var searchDir = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) - - while searchDir.pathComponents.count > 1 { - print("[COPY] Search dir: \(searchDir)") - - let gradlewFile = searchDir.appendingPathComponent("gradlew") - let gradlewExists = FileManager.default.fileExists(atPath: gradlewFile.path) - guard gradlewExists else { - searchDir = searchDir.deletingLastPathComponent() - continue - } - - let gradlewBatFile = searchDir.appendingPathComponent("gradlew.bat") - let gradlewBatExists = FileManager.default.fileExists(atPath: gradlewFile.path) - - let gradleDir = searchDir.appendingPathComponent("gradle") - let gradleDirExists = FileManager.default.fileExists(atPath: gradleDir.path) - guard gradleDirExists else { - searchDir = searchDir.deletingLastPathComponent() - continue - } - - // TODO: gradle.bat as well - try? FileManager.default.copyItem( - at: gradlewFile, - to: resolverWorkDirectory.appendingPathComponent("gradlew")) - if gradlewBatExists { - try? FileManager.default.copyItem( - at: gradlewBatFile, - to: resolverWorkDirectory.appendingPathComponent("gradlew.bat")) - } - try? FileManager.default.copyItem( - at: gradleDir, - to: resolverWorkDirectory.appendingPathComponent("gradle")) - return - } - } - - func createTemporaryDirectory(in directory: URL) throws -> URL { - let uuid = UUID().uuidString - let resolverDirectoryURL = directory.appendingPathComponent("swift-java-dependencies-\(uuid)") - - try FileManager.default.createDirectory(at: resolverDirectoryURL, withIntermediateDirectories: true, attributes: nil) - - return resolverDirectoryURL - } - -} - -struct ResolvedDependencyClasspath: CustomStringConvertible { - /// The dependency identifiers this is the classpath for. - let rootDependencies: [JavaDependencyDescriptor] - - /// Plain string representation of a Java classpath - let classpath: String - - var classpathEntries: [String] { - classpath.split(separator: ":").map(String.init) - } - - init(for rootDependencies: [JavaDependencyDescriptor], classpath: String) { - self.rootDependencies = rootDependencies - self.classpath = classpath - } - - var description: String { - "JavaClasspath(for: \(rootDependencies), classpath: \(classpath))" - } -} - diff --git a/Sources/SwiftJavaTool/SwiftJava+GenerateWrappers.swift b/Sources/SwiftJavaTool/SwiftJava+GenerateWrappers.swift deleted file mode 100644 index a57644dea..000000000 --- a/Sources/SwiftJavaTool/SwiftJava+GenerateWrappers.swift +++ /dev/null @@ -1,144 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift.org project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift.org project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import Foundation -import ArgumentParser -import SwiftJavaLib -import JavaKit -import JavaKitJar -import SwiftJavaLib -import JavaKitConfigurationShared - -extension SwiftJava { - mutating func generateWrappers( - config: Configuration, - classpath: String, - dependentConfigs: [(String, Configuration)], - environment: JNIEnvironment - ) throws { - guard let moduleName else { - fatalError("--module-name must be set in 'generate wrappers' mode!") - } - let translator = JavaTranslator( - swiftModuleName: moduleName, - environment: environment, - translateAsClass: true - ) - - // Keep track of all of the Java classes that will have - // Swift-native implementations. - translator.swiftNativeImplementations = Set(swiftNativeImplementation) - - // Note all of the dependent configurations. - for (swiftModuleName, dependentConfig) in dependentConfigs { - translator.addConfiguration( - dependentConfig, - forSwiftModule: swiftModuleName - ) - } - - // Add the configuration for this module. - translator.addConfiguration(config, forSwiftModule: moduleName) - - // Load all of the explicitly-requested classes. - let classLoader = try JavaClass(environment: environment) - .getSystemClassLoader()! - var javaClasses: [JavaClass] = [] - for (javaClassName, _) in config.classes ?? [:] { - guard let javaClass = try classLoader.loadClass(javaClassName) else { - print("warning: could not find Java class '\(javaClassName)'") - continue - } - - // Add this class to the list of classes we'll translate. - javaClasses.append(javaClass) - } - - // Find all of the nested classes for each class, adding them to the list - // of classes to be translated if they were already specified. - var allClassesToVisit = javaClasses - var currentClassIndex: Int = 0 - while currentClassIndex < allClassesToVisit.count { - defer { - currentClassIndex += 1 - } - - // The current class we're in. - let currentClass = allClassesToVisit[currentClassIndex] - guard let currentSwiftName = translator.translatedClasses[currentClass.getName()]?.swiftType else { - continue - } - - // Find all of the nested classes that weren't explicitly translated - // already. - let nestedClasses: [JavaClass] = currentClass.getClasses().compactMap { nestedClass in - guard let nestedClass else { return nil } - - // If this is a local class, we're done. - let javaClassName = nestedClass.getName() - if javaClassName.isLocalJavaClass { - return nil - } - - // If this class has been explicitly mentioned, we're done. - if translator.translatedClasses[javaClassName] != nil { - return nil - } - - // Record this as a translated class. - let swiftUnqualifiedName = javaClassName.javaClassNameToCanonicalName - .defaultSwiftNameForJavaClass - - - let swiftName = "\(currentSwiftName).\(swiftUnqualifiedName)" - translator.translatedClasses[javaClassName] = (swiftName, nil) - return nestedClass - } - - // If there were no new nested classes, there's nothing to do. - if nestedClasses.isEmpty { - continue - } - - // Record all of the nested classes that we will visit. - translator.nestedClasses[currentClass.getName()] = nestedClasses - allClassesToVisit.append(contentsOf: nestedClasses) - } - - // Validate configurations before writing any files - try translator.validateClassConfiguration() - - // Translate all of the Java classes into Swift classes. - for javaClass in javaClasses { - translator.startNewFile() - let swiftClassDecls = try translator.translateClass(javaClass) - let importDecls = translator.getImportDecls() - - let swiftFileText = """ - // Auto-generated by Java-to-Swift wrapper generator. - \(importDecls.map { $0.description }.joined()) - \(swiftClassDecls.map { $0.description }.joined(separator: "\n")) - - """ - - let swiftFileName = try! translator.getSwiftTypeName(javaClass, preferValueTypes: false) - .swiftName.replacing(".", with: "+") + ".swift" - try writeContents( - swiftFileText, - to: swiftFileName, - description: "Java class '\(javaClass.getName())' translation" - ) - } - } -} diff --git a/Sources/SwiftJavaTool/SwiftJava+JExtract.swift b/Sources/SwiftJavaTool/SwiftJava+JExtract.swift deleted file mode 100644 index 79c96d3ed..000000000 --- a/Sources/SwiftJavaTool/SwiftJava+JExtract.swift +++ /dev/null @@ -1,40 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift.org project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift.org project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import Foundation -import ArgumentParser -import SwiftJavaLib -import JavaKit -import JavaKitJar -import SwiftJavaLib -import JExtractSwiftLib -import JavaKitConfigurationShared - -/// Extract Java bindings from Swift sources or interface files. -/// -/// Example usage: -/// ``` -/// > swift-java --input-swift Sources/SwiftyBusiness \ -/// --output-swift .build/.../outputs/SwiftyBusiness \ -/// --output-Java .build/.../outputs/Java -/// ``` -extension SwiftJava { - - mutating func jextractSwift( - config: Configuration - ) throws { - try SwiftToJava(config: config).run() - } - -} diff --git a/Sources/SwiftJavaTool/SwiftJava.swift b/Sources/SwiftJavaTool/SwiftJava.swift index cae7a2a4c..ac5182a07 100644 --- a/Sources/SwiftJavaTool/SwiftJava.swift +++ b/Sources/SwiftJavaTool/SwiftJava.swift @@ -14,390 +14,54 @@ import ArgumentParser import Foundation -import SwiftJavaLib +import SwiftJavaToolLib import JExtractSwiftLib -import JavaKit -import JavaKitJar -import JavaKitNetwork -import JavaKitReflection +import SwiftJava +import JavaUtilJar +import JavaNet +import JavaLangReflect import SwiftSyntax import SwiftSyntaxBuilder -import JavaKitConfigurationShared -import JavaKitShared +import SwiftJavaConfigurationShared +import SwiftJavaShared /// Command-line utility to drive the export of Java classes into Swift types. @main struct SwiftJava: AsyncParsableCommand { static var _commandName: String { "swift-java" } - @Option(help: "The name of the Swift module into which the resulting Swift types will be generated.") - var moduleName: String? // TODO: rename to --swift-module? + static let configuration = CommandConfiguration( + abstract: "Generate sources and configuration for Swift and Java interoperability.", + subcommands: [ + ConfigureCommand.self, + ResolveCommand.self, + WrapJavaCommand.self, + JExtractCommand.self + ]) - @Option( - help: - "A Java2Swift configuration file for a given Swift module name on which this module depends, e.g., JavaKitJar=Sources/JavaKitJar/Java2Swift.config. There should be one of these options for each Swift module that this module depends on (transitively) that contains wrapped Java sources." - ) - var dependsOn: [String] = [] - - // TODO: This should be a "make wrappers" option that just detects when we give it a jar - @Flag( - help: - "Specifies that the input is a Jar file whose public classes will be loaded. The output of Java2Swift will be a configuration file (Java2Swift.config) that can be used as input to a subsequent Java2Swift invocation to generate wrappers for those public classes." - ) - var jar: Bool = false - - @Flag(help: "Fetch dependencies from given target (containing swift-java configuration) or dependency string") - var fetch: Bool = false - - @Option( - name: [.customLong("cp"), .customLong("classpath")], - help: "Class search path of directories and zip/jar files from which Java classes can be loaded." - ) - var classpath: [String] = [] - - @Option( - help: "The names of Java classes whose declared native methods will be implemented in Swift." - ) - var swiftNativeImplementation: [String] = [] - - @Option(help: "Directory containing Swift files which should be extracted into Java bindings. Also known as 'jextract' mode. Must be paired with --output-java and --output-swift.") - var inputSwift: String? = nil - - @Option(help: "The directory where generated Swift files should be written. Generally used with jextract mode.") - var outputSwift: String? = nil - - @Option(help: "The directory where generated Java files should be written. Generally used with jextract mode.") - var outputJava: String? = nil - - @Option(help: "The Java package the generated Java code should be emitted into.") - var javaPackage: String? = nil - - @Option(help: "The mode of generation to use for the output files. Used with jextract mode.") - var mode: GenerationMode = .ffm - - // TODO: clarify this vs outputSwift (history: outputSwift is jextract, and this was java2swift) - @Option(name: .shortAndLong, help: "The directory in which to output the generated Swift files or the SwiftJava configuration file.") - var outputDirectory: String? = nil - - @Option(name: .shortAndLong, help: "Directory where to write cached values (e.g. swift-java.classpath files)") - var cacheDirectory: String? = nil - - @Option(name: .shortAndLong, help: "Configure the level of logs that should be printed") - var logLevel: Logger.Level = .info - - var effectiveCacheDirectory: String? { - if let cacheDirectory { - return cacheDirectory - } else if let outputDirectory { - return outputDirectory - } else { - return nil - } - } - - @Option(name: .shortAndLong, help: "How to handle an existing swift-java.config; by default 'overwrite' by can be changed to amending a configuration") - var existingConfig: ExistingConfigFileMode = .overwrite - public enum ExistingConfigFileMode: String, ExpressibleByArgument, Codable { - case overwrite - case amend - } - - @Option(name: .shortAndLong, help: "While scanning a classpath, inspect only types included in this package") - var javaPackageFilter: String? = nil - - @Argument( - help: "The input file, which is either a Java2Swift configuration file or (if '-jar' was specified) a Jar file." - ) - var input: String? - - /// Whether we have ensured that the output directory exists. - var createdOutputDirectory: Bool = false - - var moduleBaseDir: Foundation.URL? { - if let outputDirectory { - if outputDirectory == "-" { - return nil - } - - print("[debug][swift-java] Module base directory based on outputDirectory!") - return URL(fileURLWithPath: outputDirectory) - } - - guard let moduleName else { - return nil - } - - // Put the result into Sources/\(moduleName). - let baseDir = URL(fileURLWithPath: ".") - .appendingPathComponent("Sources", isDirectory: true) - .appendingPathComponent(moduleName, isDirectory: true) - - return baseDir - } - - /// The output directory in which to place the generated files, which will - /// be the specified directory (--output-directory or -o option) if given, - /// or a default directory derived from the other command-line arguments. - /// - /// Returns `nil` only when we should emit the files to standard output. - var actualOutputDirectory: Foundation.URL? { - if let outputDirectory { - if outputDirectory == "-" { - return nil + public static func main() async { + do { + var command = try parseAsRoot(nil) + if var asyncCommand = command as? AsyncParsableCommand { + try await asyncCommand.run() + } else { + try command.run() } - - return URL(fileURLWithPath: outputDirectory) - } - - guard let moduleName else { - fatalError("--module-name must be set!") - } - - // Put the result into Sources/\(moduleName). - let baseDir = URL(fileURLWithPath: ".") - .appendingPathComponent("Sources", isDirectory: true) - .appendingPathComponent(moduleName, isDirectory: true) - - // For generated Swift sources, put them into a "generated" subdirectory. - // The configuration file goes at the top level. - let outputDir: Foundation.URL - if jar { - precondition(self.input != nil, "-jar mode requires path to jar to be specified as input path") - outputDir = baseDir - } else { - outputDir = baseDir - .appendingPathComponent("generated", isDirectory: true) + } catch { + print("Invocation: \(CommandLine.arguments.joined(separator: " "))") + exit(withError: error) } - - return outputDir } - /// Describes what kind of generation action is being performed by swift-java. - enum ToolMode { - /// Generate a configuration file given a Jar file. - case configuration(extraClasspath: String) // FIXME: this is more like "extract" configuration from classpath - - /// Generate Swift wrappers for Java classes based on the given - /// configuration. - case classWrappers - - /// Fetch dependencies for a module - case fetchDependencies - - /// Extract Java bindings from provided Swift sources. - case jextract // TODO: carry jextract specific config here? - } - - mutating func run() async { - guard CommandLine.arguments.count > 1 else { + mutating func run() async throws { + guard CommandLine.arguments.count >= 2 else { // there's no "default" command, print USAGE when no arguments/parameters are passed. - print("Must specify run mode.\n\(Self.helpMessage())") + print("error: Must specify mode subcommand (e.g. configure, resolve, jextract, ...).\n\n\(Self.helpMessage())") return } - print("[info][swift-java] Run: \(CommandLine.arguments.joined(separator: " "))") - print("[info][swift-java] Current work directory: \(URL(fileURLWithPath: "."))") - print("[info][swift-java] Module base directory: \(moduleBaseDir)") - do { - var earlyConfig: Configuration? - if let moduleBaseDir { - print("[debug][swift-java] Load config from module base directory: \(moduleBaseDir.path)") - earlyConfig = try readConfiguration(sourceDir: moduleBaseDir.path) - } else if let inputSwift { - print("[debug][swift-java] Load config from module swift input directory: \(inputSwift)") - earlyConfig = try readConfiguration(sourceDir: inputSwift) - } - var config = earlyConfig ?? Configuration() - - config.logLevel = self.logLevel - if let javaPackage { - config.javaPackage = javaPackage - } - - // Determine the mode in which we'll execute. - let toolMode: ToolMode - // TODO: some options are exclusive to each other so we should detect that - if let inputSwift { - guard let outputSwift else { - print("[swift-java] --input-swift enabled 'jextract' mode, however no --output-swift directory was provided!\n\(Self.helpMessage())") - return - } - guard let outputJava else { - print("[swift-java] --input-swift enabled 'jextract' mode, however no --output-java directory was provided!\n\(Self.helpMessage())") - return - } - config.swiftModule = self.moduleName // FIXME: rename the moduleName - config.inputSwiftDirectory = self.inputSwift - config.outputSwiftDirectory = self.outputSwift - config.outputJavaDirectory = self.outputJava - config.mode = self.mode - - toolMode = .jextract - } else if jar { - guard let input else { - fatalError("Mode -jar requires path\n\(Self.helpMessage())") - } - toolMode = .configuration(extraClasspath: input) - } else if fetch { - guard let input else { - fatalError("Mode 'fetch' requires path\n\(Self.helpMessage())") - } - config = try JavaTranslator.readConfiguration(from: URL(fileURLWithPath: input)) - guard let dependencies = config.dependencies else { - print("[swift-java] Running in 'fetch dependencies' mode but dependencies list was empty!") - print("[swift-java] Nothing to do: done.") - return - } - toolMode = .fetchDependencies - } else { - guard let input else { - fatalError("Mode -jar requires path\n\(Self.helpMessage())") - } - config = try JavaTranslator.readConfiguration(from: URL(fileURLWithPath: input)) - toolMode = .classWrappers - } - - print("[debug][swift-java] Running swift-java in mode: " + "\(toolMode.prettyName)".bold) - - let moduleName: String = - if let name = self.moduleName { - name - } else if let input { - input.split(separator: "/").dropLast().last.map(String.init) ?? "__UnknownModule" - } else { - "__UnknownModule" - } - - // Load all of the dependent configurations and associate them with Swift - // modules. - let dependentConfigs = try dependsOn.map { dependentConfig in - guard let equalLoc = dependentConfig.firstIndex(of: "=") else { - throw JavaToSwiftError.badConfigOption(dependentConfig) - } - - let afterEqual = dependentConfig.index(after: equalLoc) - let swiftModuleName = String(dependentConfig[.. (javaClassName: String, swiftName: String) { @@ -421,100 +85,18 @@ struct SwiftJava: AsyncParsableCommand { return (javaClassName, swiftName.javaClassNameToCanonicalName) } - mutating func writeContents( - _ contents: String, - to filename: String, description: String) throws { - try writeContents( - contents, - outputDirectoryOverride: self.actualOutputDirectory, - to: filename, - description: description) - } - - mutating func writeContents( - _ contents: String, - outputDirectoryOverride: Foundation.URL?, - to filename: String, - description: String) throws { - guard let outputDir = (outputDirectoryOverride ?? actualOutputDirectory) else { - print("// \(filename) - \(description)") - print(contents) - return - } - - // If we haven't tried to create the output directory yet, do so now before - // we write any files to it. - if !createdOutputDirectory { - try FileManager.default.createDirectory( - at: outputDir, - withIntermediateDirectories: true - ) - createdOutputDirectory = true - } - - // Write the file: - let file = outputDir.appendingPathComponent(filename) - print("[debug][swift-java] Writing \(description) to '\(file.path)'... ", terminator: "") - try contents.write(to: file, atomically: true, encoding: .utf8) - print("done.".green) - } -} - -extension SwiftJava { - /// Get base configuration, depending on if we are to 'amend' or 'overwrite' the existing configuration. - package func getBaseConfigurationForWrite() throws -> (Bool, Configuration) { - guard let actualOutputDirectory = self.actualOutputDirectory else { - // If output has no path there's nothing to amend - return (false, .init()) - } - - switch self.existingConfig { - case .overwrite: - // always make up a fresh instance if we're overwriting - return (false, .init()) - case .amend: - let configPath = actualOutputDirectory - guard let config = try readConfiguration(sourceDir: configPath.path) else { - return (false, .init()) - } - return (true, config) - } - } } enum JavaToSwiftError: Error { - case badConfigOption(String) + case badConfigOption } extension JavaToSwiftError: CustomStringConvertible { var description: String { switch self { - case .badConfigOption(_): + case .badConfigOption: "configuration option must be of the form '=" } } } -@JavaClass("java.lang.ClassLoader") -public struct ClassLoader { - @JavaMethod - public func loadClass(_ arg0: String) throws -> JavaClass? -} - -extension JavaClass { - @JavaStaticMethod - public func getSystemClassLoader() -> ClassLoader? -} - -extension SwiftJava.ToolMode { - var prettyName: String { - switch self { - case .configuration: "Configuration" - case .fetchDependencies: "Fetch dependencies" - case .classWrappers: "Wrap Java classes" - case .jextract: "JExtract Swift for Java" - } - } -} - -extension GenerationMode: ExpressibleByArgument {} diff --git a/Sources/SwiftJavaTool/SwiftJavaBaseAsyncParsableCommand.swift b/Sources/SwiftJavaTool/SwiftJavaBaseAsyncParsableCommand.swift new file mode 100644 index 000000000..92818e43a --- /dev/null +++ b/Sources/SwiftJavaTool/SwiftJavaBaseAsyncParsableCommand.swift @@ -0,0 +1,188 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import ArgumentParser +import Foundation +import SwiftJavaToolLib +import JExtractSwiftLib +import SwiftJava +import JavaUtilJar +import JavaNet +import JavaLangReflect +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftJavaConfigurationShared +import SwiftJavaShared +import Logging + +protocol SwiftJavaBaseAsyncParsableCommand: AsyncParsableCommand { + + var log: Logging.Logger { get } + + var logLevel: JExtractSwiftLib.Logger.Level { get set } + + /// Must be implemented with an `@OptionGroup` in Command implementations + var commonOptions: SwiftJava.CommonOptions { get set } + + var effectiveSwiftModule: String { get } + + mutating func runSwiftJavaCommand(config: inout Configuration) async throws + +} + +extension SwiftJavaBaseAsyncParsableCommand { + var outputDirectory: String? { + self.commonOptions.outputDirectory + } +} + +extension SwiftJavaBaseAsyncParsableCommand { + public mutating func run() async { + self.log.info("Run \(Self.self): \(CommandLine.arguments.joined(separator: " "))") + self.log.info("Current work directory: \(URL(fileURLWithPath: ".").path)") + + do { + var config = try readInitialConfiguration(command: self) + try await runSwiftJavaCommand(config: &config) + } catch { + // We fail like this since throwing out of the run often ends up hiding the failure reason when it is executed as SwiftPM plugin (!) + let message = "Failed with error: \(error)" + self.log.error("\(message)") + fatalError(message) + } + + // Just for debugging so it is clear which command has finished + self.log.debug("\("Done: ".green) \(CommandLine.arguments.joined(separator: " ").green)") + } +} + +extension SwiftJavaBaseAsyncParsableCommand { + mutating func writeContents( + _ contents: String, + outputDirectory: Foundation.URL?, + to filename: String, + description: String) throws { + guard let outputDir = outputDirectory else { + print("// \(filename) - \(description)") + print(contents) + return + } + + // If we haven't tried to create the output directory yet, do so now before + // we write any files to it. + // if !createdOutputDirectory { + try FileManager.default.createDirectory( + at: outputDir, + withIntermediateDirectories: true + ) + // createdOutputDirectory = true + //} + + // Write the file: + let file = outputDir.appendingPathComponent(filename) + print("[trace][swift-java] Writing \(description) to '\(file.path)'... ", terminator: "") + try contents.write(to: file, atomically: true, encoding: .utf8) + print("done.".green) + } +} + + +extension SwiftJavaBaseAsyncParsableCommand { + var log: Logging.Logger { // FIXME: replace with stored property inside specific commands + .init(label: "swift-java") + } + + var logLevel: JExtractSwiftLib.Logger.Level { + get { + self.commonOptions.logLevel + } + set { + self.commonOptions.logLevel = newValue + } + } + + var effectiveSwiftModuleURL: Foundation.URL { + let fm = FileManager.default + return URL(fileURLWithPath: fm.currentDirectoryPath + "/Sources/\(self.effectiveSwiftModule)") + } +} +extension SwiftJavaBaseAsyncParsableCommand { + + var moduleBaseDir: Foundation.URL? { + if let outputDirectory = commonOptions.outputDirectory { + if outputDirectory == "-" { + return nil + } +// print("[debug][swift-java] Module base directory based on outputDirectory!") +// return URL(fileURLWithPath: outputDirectory) + } + + // Put the result into Sources/\(swiftModule). + let baseDir = URL(fileURLWithPath: ".") + .appendingPathComponent("Sources", isDirectory: true) + .appendingPathComponent(self.effectiveSwiftModule, isDirectory: true) + + return baseDir + } + + /// The output directory in which to place the generated files, which will + /// be the specified directory (--output-directory or -o option) if given, + /// or a default directory derived from the other command-line arguments. + /// + /// Returns `nil` only when we should emit the files to standard output. + var actualOutputDirectory: Foundation.URL? { + if let outputDirectory = self.commonOptions.outputDirectory { + if outputDirectory == "-" { + return nil + } + + return URL(fileURLWithPath: outputDirectory) + } + + // Put the result into Sources/\(swiftModule). + let baseDir = URL(fileURLWithPath: ".") + .appendingPathComponent("Sources", isDirectory: true) + .appendingPathComponent(effectiveSwiftModule, isDirectory: true) + + // For generated Swift sources, put them into a "generated" subdirectory. + // The configuration file goes at the top level. + let outputDir: Foundation.URL = baseDir + return outputDir + } + + func readInitialConfiguration(command: some SwiftJavaBaseAsyncParsableCommand) throws -> Configuration { + var earlyConfig: Configuration? + if let configPath = commonOptions.config { + let configURL = URL(filePath: configPath, directoryHint: .notDirectory) + print("[debug][swift-java] Load config from passed in path: \(configURL)") + earlyConfig = try readConfiguration(configPath: configURL) + } else if let moduleBaseDir { + print("[debug][swift-java] Load config from module base directory: \(moduleBaseDir.path)") + earlyConfig = try readConfiguration(sourceDir: moduleBaseDir.path) + } else if let inputSwift = commonOptions.inputSwift { + print("[debug][swift-java] Load config from module swift input directory: \(inputSwift)") + earlyConfig = try readConfiguration(sourceDir: inputSwift) + } + var config: Configuration + if let earlyConfig { + config = earlyConfig + } else { + log.warning("[swift-java] Failed to load initial configuration. Proceeding with empty configuration.") + config = Configuration() + } + // override configuration with options from command line + config.logLevel = command.logLevel + return config + } +} diff --git a/Sources/SwiftJavaLib/JavaClassTranslator.swift b/Sources/SwiftJavaToolLib/JavaClassTranslator.swift similarity index 75% rename from Sources/SwiftJavaLib/JavaClassTranslator.swift rename to Sources/SwiftJavaToolLib/JavaClassTranslator.swift index bfd1657f5..ce780f904 100644 --- a/Sources/SwiftJavaLib/JavaClassTranslator.swift +++ b/Sources/SwiftJavaToolLib/JavaClassTranslator.swift @@ -12,9 +12,12 @@ // //===----------------------------------------------------------------------===// -import JavaKit -import JavaKitReflection +import SwiftJava +import JavaLangReflect import SwiftSyntax +import OrderedCollections +import SwiftJavaConfigurationShared +import Logging /// Utility type that translates a single Java class into its corresponding /// Swift type and any additional helper types or functions. @@ -23,6 +26,10 @@ struct JavaClassTranslator { /// needed for translation. let translator: JavaTranslator + var log: Logger { + translator.log + } + /// The Java class (or interface) being translated. let javaClass: JavaClass @@ -47,7 +54,7 @@ struct JavaClassTranslator { let effectiveJavaSuperclass: JavaClass? /// The Swift name of the superclass. - let swiftSuperclass: String? + let swiftSuperclass: SwiftJavaParameterizedType? /// The Swift names of the interfaces that this class implements. let swiftInterfaces: [String] @@ -98,16 +105,16 @@ struct JavaClassTranslator { } /// The generic parameter clause for the Swift version of the Java class. - var genericParameterClause: String { + var genericParameters: [String] { if javaTypeParameters.isEmpty { - return "" + return [] } let genericParameters = javaTypeParameters.map { param in "\(param.getName()): AnyJavaObject" } - return "<\(genericParameters.joined(separator: ", "))>" + return genericParameters } /// Prepare translation for the given Java class (or interface). @@ -126,23 +133,38 @@ struct JavaClassTranslator { self.javaTypeParameters = javaClass.getTypeParameters().compactMap { $0 } self.nestedClasses = translator.nestedClasses[fullName] ?? [] - // Superclass. + // Superclass, incl parameter types (if any) if !javaClass.isInterface() { var javaSuperclass = javaClass.getSuperclass() - var swiftSuperclass: String? = nil + var javaGenericSuperclass: JavaReflectType? = javaClass.getGenericSuperclass() + var swiftSuperclassName: String? = nil + var swiftSuperclassTypeArgs: [String] = [] while let javaSuperclassNonOpt = javaSuperclass { do { - swiftSuperclass = try translator.getSwiftTypeName(javaSuperclassNonOpt, preferValueTypes: false).swiftName + swiftSuperclassName = try translator.getSwiftTypeName(javaSuperclassNonOpt, preferValueTypes: false).swiftName + if let javaGenericSuperclass = javaGenericSuperclass?.as(JavaReflectParameterizedType.self) { + for typeArg in javaGenericSuperclass.getActualTypeArguments() { + let javaTypeArgName = typeArg?.getTypeName() ?? "" + if let swiftTypeArgName = self.translator.translatedClasses[javaTypeArgName] { + swiftSuperclassTypeArgs.append(swiftTypeArgName.qualifiedName) + } else { + swiftSuperclassTypeArgs.append("/* MISSING MAPPING FOR */ \(javaTypeArgName)") + } + } + } break } catch { translator.logUntranslated("Unable to translate '\(fullName)' superclass: \(error)") } javaSuperclass = javaSuperclassNonOpt.getSuperclass() + javaGenericSuperclass = javaClass.getGenericSuperclass() } self.effectiveJavaSuperclass = javaSuperclass - self.swiftSuperclass = swiftSuperclass + self.swiftSuperclass = SwiftJavaParameterizedType( + name: swiftSuperclassName, + typeArguments: swiftSuperclassTypeArgs) } else { self.effectiveJavaSuperclass = nil self.swiftSuperclass = nil @@ -192,8 +214,9 @@ struct JavaClassTranslator { for method in methods { guard let method else { continue } - // Only look at public and protected methods here. - guard method.isPublic || method.isProtected else { continue } + guard shouldExtract(method: method) else { + continue + } // Skip any methods that are expected to be implemented in Swift. We will // visit them in the second pass, over the *declared* methods, because @@ -205,6 +228,11 @@ struct JavaClassTranslator { continue } + guard method.getName().isValidSwiftFunctionName else { + log.warning("Skipping method \(method.getName()) because it is not a valid Swift function name") + continue + } + addMethod(method, isNative: false) } @@ -226,6 +254,20 @@ struct JavaClassTranslator { /// MARK: Collection of Java class members. extension JavaClassTranslator { + + /// Determines whether a method should be extracted for translation. + /// Only look at public and protected methods here. + private func shouldExtract(method: Method) -> Bool { + switch self.translator.config.effectiveMinimumInputAccessLevelMode { + case .internal: + return method.isPublic || method.isProtected || method.isPackage + case .package: + return method.isPublic || method.isProtected || method.isPackage + case .public: + return method.isPublic || method.isProtected + } + } + /// Add a field to the appropriate lists(s) for later translation. private mutating func addField(_ field: Field) { // Static fields go into a separate list. @@ -325,13 +367,25 @@ extension JavaClassTranslator { // Compute the "extends" clause for the superclass (of the struct // formulation) or the inheritance clause (for the class // formulation). - let extends: String + let extendsClause: String let inheritanceClause: String if translateAsClass { - extends = "" - inheritanceClause = swiftSuperclass.map { ": \($0)" } ?? "" + extendsClause = "" + inheritanceClause = + if let swiftSuperclass, swiftSuperclass.typeArguments.isEmpty { + ": \(swiftSuperclass.name)" + } else if let swiftSuperclass { + ": \(swiftSuperclass.name)<\(swiftSuperclass.typeArguments.joined(separator: ", "))>" + } else { + "" + } } else { - extends = swiftSuperclass.map { ", extends: \($0).self" } ?? "" + extendsClause = + if let swiftSuperclass { + ", extends: \(swiftSuperclass.render()).self" + } else { + "" + } inheritanceClause = "" } @@ -344,12 +398,19 @@ extension JavaClassTranslator { interfacesStr = ", \(prefix): \(swiftInterfaces.map { "\($0).self" }.joined(separator: ", "))" } + let genericParameterClause = + if genericParameters.isEmpty { + "" + } else { + "<\(genericParameters.joined(separator: ", "))>" + } + // Emit the struct declaration describing the java class. let classOrInterface: String = isInterface ? "JavaInterface" : "JavaClass"; let introducer = translateAsClass ? "open class" : "public struct" var classDecl: DeclSyntax = """ - @\(raw: classOrInterface)(\(literal: javaClass.getName())\(raw: extends)\(raw: interfacesStr)) + @\(raw: classOrInterface)(\(literal: javaClass.getName())\(raw: extendsClause)\(raw: interfacesStr)) \(raw: introducer) \(raw: swiftInnermostTypeName)\(raw: genericParameterClause)\(raw: inheritanceClause) { \(raw: members.map { $0.description }.joined(separator: "\n\n")) } @@ -396,7 +457,7 @@ extension JavaClassTranslator { } let genericArgumentClause = "<\(genericParameterNames.joined(separator: ", "))>" - staticMemberWhereClause = " where ObjectType == \(swiftTypeName)\(genericArgumentClause)" + staticMemberWhereClause = " where ObjectType == \(swiftTypeName)\(genericArgumentClause)" // FIXME: move the 'where ...' part into the render bit } else { staticMemberWhereClause = "" } @@ -418,7 +479,7 @@ extension JavaClassTranslator { do { return try renderMethod( method, implementedInSwift: /*FIXME:*/false, - genericParameterClause: genericParameterClause, + genericParameters: genericParameters, whereClause: staticMemberWhereClause ) } catch { @@ -435,7 +496,7 @@ extension JavaClassTranslator { // Specify the specialization arguments when needed. let extSpecialization: String - if genericParameterClause.isEmpty { + if genericParameters.isEmpty { extSpecialization = "<\(swiftTypeName)>" } else { extSpecialization = "" @@ -517,37 +578,99 @@ extension JavaClassTranslator { package func renderConstructor( _ javaConstructor: Constructor ) throws -> DeclSyntax { - let parameters = try translateParameters(javaConstructor.getParameters()) + ["environment: JNIEnvironment? = nil"] + let parameters = try translateJavaParameters(javaConstructor.getParameters()) + ["environment: JNIEnvironment? = nil"] let parametersStr = parameters.map { $0.description }.joined(separator: ", ") let throwsStr = javaConstructor.throwsCheckedException ? "throws" : "" let accessModifier = javaConstructor.isPublic ? "public " : "" let convenienceModifier = translateAsClass ? "convenience " : "" let nonoverrideAttribute = translateAsClass ? "@_nonoverride " : "" + + // FIXME: handle generics in constructors return """ @JavaMethod \(raw: nonoverrideAttribute)\(raw: accessModifier)\(raw: convenienceModifier)init(\(raw: parametersStr))\(raw: throwsStr) """ } + func genericParameterIsUsedInSignature(_ typeParam: TypeVariable, in method: Method) -> Bool { + // --- Return type + // Is the return type exactly the type param + // FIXME: make this equals based? + if method.getGenericReturnType().getTypeName() == typeParam.getTypeName() { + return true + } + + if let parameterizedReturnType = method.getGenericReturnType().as(ParameterizedType.self) { + for actualTypeParam in parameterizedReturnType.getActualTypeArguments() { + guard let actualTypeParam else { continue } + if actualTypeParam.isEqualTo(typeParam.as(Type.self)) { + return true + } + } + } + + // --- Parameter types + for parameter in method.getParameters() { + if let parameterizedType = parameter?.getParameterizedType() { + if parameterizedType.isEqualTo(typeParam.as(Type.self)) { + return true + } + } + } + + return false + } + + // TODO: make it more precise with the "origin" of the generic parameter (outer class etc) + func collectMethodGenericParameters( + genericParameters: [String], + method: Method + ) -> OrderedSet { + var allGenericParameters = OrderedSet(genericParameters) + + let typeParameters = method.getTypeParameters() + for typeParameter in typeParameters { + guard let typeParameter else { continue } + + guard genericParameterIsUsedInSignature(typeParameter, in: method) else { + continue + } + + allGenericParameters.append("\(typeParameter.getTypeName()): AnyJavaObject") + } + + return allGenericParameters + } + /// Translates the given Java method into a Swift declaration. package func renderMethod( _ javaMethod: Method, implementedInSwift: Bool, - genericParameterClause: String = "", + genericParameters: [String] = [], whereClause: String = "" ) throws -> DeclSyntax { - // Map the parameters. - let parameters = try translateParameters(javaMethod.getParameters()) + // Map the generic params on the method. + let allGenericParameters = collectMethodGenericParameters(genericParameters: genericParameters, method: javaMethod) + let genericParameterClauseStr = + if allGenericParameters.isEmpty { + "" + } else { + "<\(allGenericParameters.joined(separator: ", "))>" + } + // Map the parameters. + let parameters = try translateJavaParameters(javaMethod) let parametersStr = parameters.map { $0.description }.joined(separator: ", ") // Map the result type. let resultTypeStr: String - let resultType = try translator.getSwiftTypeNameAsString( - javaMethod.getGenericReturnType()!, - preferValueTypes: true, + let resultType = try translator.getSwiftReturnTypeNameAsString( + method: javaMethod, + preferValueTypes: true, outerOptional: .implicitlyUnwrappedOptional ) + let hasTypeEraseGenericResultType: Bool = + isTypeErased(javaMethod.getGenericReturnType()) // FIXME: cleanup the checking here if resultType != "Void" && resultType != "Swift.Void" { @@ -556,11 +679,37 @@ extension JavaClassTranslator { resultTypeStr = "" } + // --- Handle other effects let throwsStr = javaMethod.throwsCheckedException ? "throws" : "" let swiftMethodName = javaMethod.getName().escapedSwiftName - let methodAttribute: AttributeSyntax = implementedInSwift - ? "" - : javaMethod.isStatic ? "@JavaStaticMethod\n" : "@JavaMethod\n"; + + // Compute the parameters for '@...JavaMethod(...)' + let methodAttribute: AttributeSyntax + if implementedInSwift { + methodAttribute = "" + } else { + var methodAttributeStr = + if javaMethod.isStatic { + "@JavaStaticMethod" + } else { + "@JavaMethod" + } + // Do we need to record any generic information, in order to enable type-erasure for the upcalls? + var parameters: [String] = [] + if hasTypeEraseGenericResultType { + parameters.append("typeErasedResult: \"\(resultType)\"") + } + // TODO: generic parameters? + + if !parameters.isEmpty { + methodAttributeStr += "(" + methodAttributeStr.append(parameters.joined(separator: ", ")) + methodAttributeStr += ")" + } + methodAttributeStr += "\n" + methodAttribute = "\(raw: methodAttributeStr)" + } + let accessModifier = implementedInSwift ? "" : (javaMethod.isStatic || !translateAsClass) ? "public " : "open " @@ -568,6 +717,7 @@ extension JavaClassTranslator { ? "override " : "" + // FIXME: refactor this so we don't have to duplicate the method signatures if resultType.optionalWrappedType() != nil || parameters.contains(where: { $0.type.trimmedDescription.optionalWrappedType() != nil }) { let parameters = parameters.map { param -> (clause: FunctionParameterSyntax, passedArg: String) in let name = param.secondName!.trimmedDescription @@ -581,23 +731,26 @@ extension JavaClassTranslator { let resultOptional: String = resultType.optionalWrappedType() ?? resultType let baseBody: ExprSyntax = "\(raw: javaMethod.throwsCheckedException ? "try " : "")\(raw: swiftMethodName)(\(raw: parameters.map(\.passedArg).joined(separator: ", ")))" - let body: ExprSyntax = if let optionalType = resultType.optionalWrappedType() { - "Optional(javaOptional: \(baseBody))" - } else { - baseBody - } + let body: ExprSyntax = + if resultType.optionalWrappedType() != nil { + "Optional(javaOptional: \(baseBody))" + } else { + baseBody + } - return """ - \(methodAttribute)\(raw: accessModifier)\(raw: overrideOpt)func \(raw: swiftMethodName)\(raw: genericParameterClause)(\(raw: parametersStr))\(raw: throwsStr)\(raw: resultTypeStr)\(raw: whereClause) + return + """ + \(methodAttribute)\(raw: accessModifier)\(raw: overrideOpt)func \(raw: swiftMethodName)\(raw: genericParameterClauseStr)(\(raw: parametersStr))\(raw: throwsStr)\(raw: resultTypeStr)\(raw: whereClause) - \(raw: accessModifier)\(raw: overrideOpt)func \(raw: swiftMethodName)Optional\(raw: genericParameterClause)(\(raw: parameters.map(\.clause.description).joined(separator: ", ")))\(raw: throwsStr) -> \(raw: resultOptional)\(raw: whereClause) { + \(raw: accessModifier)\(raw: overrideOpt)func \(raw: swiftMethodName)Optional\(raw: genericParameterClauseStr)(\(raw: parameters.map(\.clause.description).joined(separator: ", ")))\(raw: throwsStr) -> \(raw: resultOptional)\(raw: whereClause) { \(body) } """ } else { - return """ - \(methodAttribute)\(raw: accessModifier)\(raw: overrideOpt)func \(raw: swiftMethodName)\(raw: genericParameterClause)(\(raw: parametersStr))\(raw: throwsStr)\(raw: resultTypeStr)\(raw: whereClause) + return + """ + \(methodAttribute)\(raw: accessModifier)\(raw: overrideOpt)func \(raw: swiftMethodName)\(raw: genericParameterClauseStr)(\(raw: parametersStr))\(raw: throwsStr)\(raw: resultTypeStr)\(raw: whereClause) """ } } @@ -688,7 +841,7 @@ extension JavaClassTranslator { ? "self.init(javaHolder: \($0.getName()).javaHolder)" : "self = \($0.getName())") } else { - fatalError("Enum value \($0.getName()) was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value \($0.getName()) was unexpectedly nil, please re-run swift-java on the most updated Java class") } """ }.joined(separator: "\n")) @@ -700,7 +853,30 @@ extension JavaClassTranslator { } // Translate a Java parameter list into Swift parameters. - private func translateParameters(_ parameters: [Parameter?]) throws -> [FunctionParameterSyntax] { + private func translateJavaParameters( + _ javaMethod: JavaLangReflect.Method + ) throws -> [FunctionParameterSyntax] { + let parameters: [Parameter?] = javaMethod.getParameters() + + return try parameters.compactMap { javaParameter in + guard let javaParameter else { return nil } + + let typeName = try translator.getSwiftTypeNameAsString( + method: javaMethod, + javaParameter.getParameterizedType()!, + preferValueTypes: true, + outerOptional: .optional + ) + let paramName = javaParameter.getName() + return "_ \(raw: paramName): \(raw: typeName)" + } + } + + // Translate a Java parameter list into Swift parameters. + @available(*, deprecated, message: "Prefer the method based version") // FIXME: constructors are not well handled + private func translateJavaParameters( + _ parameters: [Parameter?] + ) throws -> [FunctionParameterSyntax] { return try parameters.compactMap { javaParameter in guard let javaParameter else { return nil } @@ -754,6 +930,7 @@ struct MethodCollector { } // MARK: Utility functions + extension JavaClassTranslator { /// Determine whether this method is an override of another Java /// method. @@ -778,9 +955,7 @@ extension JavaClassTranslator { continue } - // Ignore non-public, non-protected methods because they would not - // have been render into the Swift superclass. - if !overriddenMethod.isPublic && !overriddenMethod.isProtected { + guard shouldExtract(method: overriddenMethod) else { continue } @@ -791,6 +966,7 @@ extension JavaClassTranslator { return true } } catch { + log.debug("Failed to determine if method '\(method)' is an override, error: \(error)") } } @@ -815,114 +991,3 @@ extension [Type?] { } } -extension Type { - /// Adjust the given type to use its bounds, mirroring what we do in - /// mapping Java types into Swift. - func adjustToJavaBounds(adjusted: inout Bool) -> Type { - if let typeVariable = self.as(TypeVariable.self), - typeVariable.getBounds().count == 1, - let bound = typeVariable.getBounds()[0] { - adjusted = true - return bound - } - - if let wildcardType = self.as(WildcardType.self), - wildcardType.getUpperBounds().count == 1, - let bound = wildcardType.getUpperBounds()[0] { - adjusted = true - return bound - } - - return self - } - - /// Determine whether this type is equivalent to or a subtype of the other - /// type. - func isEqualTo(_ other: Type) -> Bool { - // First, adjust types to their bounds, if we need to. - var anyAdjusted: Bool = false - let adjustedSelf = self.adjustToJavaBounds(adjusted: &anyAdjusted) - let adjustedOther = other.adjustToJavaBounds(adjusted: &anyAdjusted) - if anyAdjusted { - return adjustedSelf.isEqualTo(adjustedOther) - } - - // If both are classes, check for equivalence. - if let selfClass = self.as(JavaClass.self), - let otherClass = other.as(JavaClass.self) { - return selfClass.equals(otherClass.as(JavaObject.self)) - } - - // If both are arrays, check that their component types are equivalent. - if let selfArray = self.as(GenericArrayType.self), - let otherArray = other.as(GenericArrayType.self) { - return selfArray.getGenericComponentType().isEqualTo(otherArray.getGenericComponentType()) - } - - // If both are parameterized types, check their raw type and type - // arguments for equivalence. - if let selfParameterizedType = self.as(ParameterizedType.self), - let otherParameterizedType = other.as(ParameterizedType.self) { - if !selfParameterizedType.getRawType().isEqualTo(otherParameterizedType.getRawType()) { - return false - } - - return selfParameterizedType.getActualTypeArguments() - .allTypesEqual(otherParameterizedType.getActualTypeArguments()) - } - - // If both are type variables, compare their bounds. - // FIXME: This is a hack. - if let selfTypeVariable = self.as(TypeVariable.self), - let otherTypeVariable = other.as(TypeVariable.self) { - return selfTypeVariable.getBounds().allTypesEqual(otherTypeVariable.getBounds()) - } - - // If both are wildcards, compare their upper and lower bounds. - if let selfWildcard = self.as(WildcardType.self), - let otherWildcard = other.as(WildcardType.self) { - return selfWildcard.getUpperBounds().allTypesEqual(otherWildcard.getUpperBounds()) - && selfWildcard.getLowerBounds().allTypesEqual(otherWildcard.getLowerBounds()) - } - - return false - } - - /// Determine whether this type is equivalent to or a subtype of the - /// other type. - func isEqualToOrSubtypeOf(_ other: Type) -> Bool { - // First, adjust types to their bounds, if we need to. - var anyAdjusted: Bool = false - let adjustedSelf = self.adjustToJavaBounds(adjusted: &anyAdjusted) - let adjustedOther = other.adjustToJavaBounds(adjusted: &anyAdjusted) - if anyAdjusted { - return adjustedSelf.isEqualToOrSubtypeOf(adjustedOther) - } - - if isEqualTo(other) { - return true - } - - // If both are classes, check for subclassing. - if let selfClass = self.as(JavaClass.self), - let otherClass = other.as(JavaClass.self) { - // If either is a Java array, then this cannot be a subtype relationship - // in Swift. - if selfClass.isArray() || otherClass.isArray() { - return false - } - - return selfClass.isSubclass(of: otherClass) - } - - // Anything object-like is a subclass of java.lang.Object - if let otherClass = other.as(JavaClass.self), - otherClass.getName() == "java.lang.Object" { - if self.is(GenericArrayType.self) || self.is(ParameterizedType.self) || - self.is(WildcardType.self) || self.is(TypeVariable.self) { - return true - } - } - return false - } -} diff --git a/Sources/SwiftJavaToolLib/JavaGenericsSupport.swift b/Sources/SwiftJavaToolLib/JavaGenericsSupport.swift new file mode 100644 index 000000000..1d847276d --- /dev/null +++ b/Sources/SwiftJavaToolLib/JavaGenericsSupport.swift @@ -0,0 +1,111 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JavaLangReflect +import JavaTypes +import SwiftBasicFormat +import SwiftJava +import SwiftJavaConfigurationShared +import SwiftSyntax +import SwiftSyntaxBuilder + +struct GenericJavaTypeOriginInfo { + enum GenericSource { + /// The source of the generic + case `class`([Type]) + case method + } + + var source: GenericSource + var type: Type +} + +/// if the type (that is used by the Method) is generic, return if the use originates from the method, or a surrounding class. +func getGenericJavaTypeOriginInfo(_ type: Type?, from method: Method) -> [GenericJavaTypeOriginInfo] { + guard let type else { + return [] + } + + guard isGenericJavaType(type) else { + return [] // it's not a generic type, no "origin" of the use to detect + } + + var methodTypeVars = method.getTypeParameters() + + // TODO: also handle nested classes here... + var classTypeVars = method.getDeclaringClass().getTypeParameters() + + var usedTypeVars: [TypeVariable] = [] + + return [] +} + +func isGenericJavaType(_ type: Type?) -> Bool { + guard let type else { + return false + } + + // Check if it's a type variable (e.g., T, E, etc.) + if type.as(TypeVariable.self) != nil { + return true + } + + // Check if it's a parameterized type (e.g., List, Map) + if let paramType = type.as(ParameterizedType.self) { + let typeArgs: [Type?] = paramType.getActualTypeArguments() + + // Check if any of the type arguments are generic + for typeArg in typeArgs { + guard let typeArg else { continue } + if isGenericJavaType(typeArg) { + return true + } + } + } + + // Check if it's a generic array type (e.g., T[], List[]) + if let arrayType = type.as(GenericArrayType.self) { + let componentType = arrayType.getGenericComponentType() + return isGenericJavaType(componentType) + } + + // Check if it's a wildcard type (e.g., ? extends Number, ? super String) + if type.as(WildcardType.self) != nil { + return true + } + + return false +} + +/// Check if a type is type-erased att runtime. +/// +/// E.g. in a method returning a generic `T` the T is type erased and must +/// be represented as a `java.lang.Object` instead. +func isTypeErased(_ type: Type?) -> Bool { + guard let type else { + return false + } + + // Check if it's a type variable (e.g., T, E, etc.) + if type.as(TypeVariable.self) != nil { + return true + } + + // Check if it's a wildcard type (e.g., ? extends Number, ? super String) + if type.as(WildcardType.self) != nil { + return true + } + + return false +} diff --git a/Sources/SwiftJavaToolLib/JavaHomeSupport.swift b/Sources/SwiftJavaToolLib/JavaHomeSupport.swift new file mode 100644 index 000000000..cd18f7a82 --- /dev/null +++ b/Sources/SwiftJavaToolLib/JavaHomeSupport.swift @@ -0,0 +1,92 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +/// Detected JAVA_HOME for this process. +package let javaHome: String = findJavaHome() + +// Note: the JAVA_HOME environment variable must be set to point to where +// Java is installed, e.g., +// Library/Java/JavaVirtualMachines/openjdk-21.jdk/Contents/Home. +public func findJavaHome() -> String { + if let home = ProcessInfo.processInfo.environment["JAVA_HOME"] { + return home + } + + // This is a workaround for envs (some IDEs) which have trouble with + // picking up env variables during the build process + let path = "\(FileManager.default.homeDirectoryForCurrentUser.path()).java_home" + if let home = try? String(contentsOfFile: path, encoding: .utf8) { + if let lastChar = home.last, lastChar.isNewline { + return String(home.dropLast()) + } + + return home + } + + if let home = getJavaHomeFromLibexecJavaHome(), + !home.isEmpty + { + return home + } + + if ProcessInfo.processInfo.environment["SPI_PROCESSING"] == "1" + && ProcessInfo.processInfo.environment["SPI_BUILD"] == nil + { + // Just ignore that we're missing a JAVA_HOME when building in Swift Package Index during general processing where no Java is needed. However, do _not_ suppress the error during SPI's compatibility build stage where Java is required. + return "" + } + fatalError("Please set the JAVA_HOME environment variable to point to where Java is installed.") +} + +/// On MacOS we can use the java_home tool as a fallback if we can't find JAVA_HOME environment variable. +public func getJavaHomeFromLibexecJavaHome() -> String? { + let task = Process() + task.executableURL = URL(fileURLWithPath: "/usr/libexec/java_home") + + // Check if the executable exists before trying to run it + guard FileManager.default.fileExists(atPath: task.executableURL!.path) else { + print("/usr/libexec/java_home does not exist") + return nil + } + + let pipe = Pipe() + task.standardOutput = pipe + task.standardError = pipe // Redirect standard error to the same pipe for simplicity + + do { + try task.run() + task.waitUntilExit() + + let data = pipe.fileHandleForReading.readDataToEndOfFile() + let output = String(data: data, encoding: .utf8)?.trimmingCharacters( + in: .whitespacesAndNewlines) + + if task.terminationStatus == 0 { + return output + } else { + print("java_home terminated with status: \(task.terminationStatus)") + // Optionally, log the error output for debugging + if let errorOutput = String( + data: pipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) { + print("Error output: \(errorOutput)") + } + return nil + } + } catch { + print("Error running java_home: \(error)") + return nil + } +} diff --git a/Sources/SwiftJavaToolLib/JavaParameterizedType.swift b/Sources/SwiftJavaToolLib/JavaParameterizedType.swift new file mode 100644 index 000000000..a1b2c6b60 --- /dev/null +++ b/Sources/SwiftJavaToolLib/JavaParameterizedType.swift @@ -0,0 +1,38 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// E.g. `Another` +struct SwiftJavaParameterizedType { + let name: String + let typeArguments: [String] + + init?(name: String?, typeArguments: [String]) { + guard let name else { + return nil + } + + self.name = name + self.typeArguments = typeArguments + } + + func render() -> String { + if typeArguments.isEmpty { + name + } else { + "\(name)<\(typeArguments.joined(separator: ", "))>" + } + } + + +} diff --git a/Sources/SwiftJavaLib/JavaTranslator+Configuration.swift b/Sources/SwiftJavaToolLib/JavaTranslator+Configuration.swift similarity index 68% rename from Sources/SwiftJavaLib/JavaTranslator+Configuration.swift rename to Sources/SwiftJavaToolLib/JavaTranslator+Configuration.swift index e0f6d0cb4..655c67650 100644 --- a/Sources/SwiftJavaLib/JavaTranslator+Configuration.swift +++ b/Sources/SwiftJavaToolLib/JavaTranslator+Configuration.swift @@ -13,14 +13,9 @@ //===----------------------------------------------------------------------===// import Foundation -import JavaKitConfigurationShared +import SwiftJavaConfigurationShared extension JavaTranslator { - /// Read a configuration file from the given URL. - package static func readConfiguration(from url: URL) throws -> Configuration { - let contents = try Data(contentsOf: url) - return try JSONDecoder().decode(Configuration.self, from: contents) - } /// Load the configuration file with the given name to populate the known set of /// translated Java classes. @@ -30,10 +25,7 @@ extension JavaTranslator { } for (javaClassName, swiftName) in classes { - translatedClasses[javaClassName] = ( - swiftType: swiftName, - swiftModule: swiftModule - ) + translatedClasses[javaClassName] = SwiftTypeName(module: swiftModule, name: swiftName) } } } diff --git a/Sources/SwiftJavaLib/JavaTranslator+Validation.swift b/Sources/SwiftJavaToolLib/JavaTranslator+Validation.swift similarity index 54% rename from Sources/SwiftJavaLib/JavaTranslator+Validation.swift rename to Sources/SwiftJavaToolLib/JavaTranslator+Validation.swift index 9d4d00ca4..8071c36ad 100644 --- a/Sources/SwiftJavaLib/JavaTranslator+Validation.swift +++ b/Sources/SwiftJavaToolLib/JavaTranslator+Validation.swift @@ -12,22 +12,41 @@ // //===----------------------------------------------------------------------===// -package extension JavaTranslator { - struct SwiftTypeName: Hashable { - let swiftType: String - let swiftModule: String? +public typealias JavaFullyQualifiedTypeName = String - package init(swiftType: String, swiftModule: String?) { - self.swiftType = swiftType - self.swiftModule = swiftModule +package struct SwiftTypeName: Hashable, CustomStringConvertible { + package let swiftModule: String? + package let swiftType: String + + package init(module: String?, name: String) { + self.swiftModule = module + self.swiftType = name + } + + package var qualifiedName: String { + if let swiftModule { + "\(swiftModule).\(swiftType)" + } else { + "\(swiftType)" } } + package var description: String { + if let swiftModule { + "`\(swiftModule)/\(swiftType)`" + } else { + "`\(swiftType)`" + } + } +} + +package extension JavaTranslator { + struct SwiftToJavaMapping: Equatable { let swiftType: SwiftTypeName - let javaTypes: [String] + let javaTypes: [JavaFullyQualifiedTypeName] - package init(swiftType: SwiftTypeName, javaTypes: [String]) { + package init(swiftType: SwiftTypeName, javaTypes: [JavaFullyQualifiedTypeName]) { self.swiftType = swiftType self.javaTypes = javaTypes } @@ -39,24 +58,28 @@ package extension JavaTranslator { package var description: String { switch self { case .multipleClassesMappedToSameName(let swiftToJavaMapping): - """ - The following Java classes were mapped to the same Swift type name: - \(swiftToJavaMapping.map(mappingDescription(mapping:)).joined(separator: "\n")) - """ + """ + The following Java classes were mapped to the same Swift type name: + \(swiftToJavaMapping.map(mappingDescription(mapping:)).joined(separator: "\n")) + """ } } private func mappingDescription(mapping: SwiftToJavaMapping) -> String { let javaTypes = mapping.javaTypes.map { "'\($0)'" }.joined(separator: ", ") - return "Swift Type: '\(mapping.swiftType.swiftModule ?? "")'.'\(mapping.swiftType.swiftType)', Java Types: \(javaTypes)" + return "Swift module: '\(mapping.swiftType.swiftModule ?? "")', type: '\(mapping.swiftType.swiftType)', Java Types: \(javaTypes)" } } func validateClassConfiguration() throws(ValidationError) { // Group all classes by swift name - let groupedDictionary: [SwiftTypeName: [(String, (String, String?))]] = Dictionary(grouping: translatedClasses, by: { SwiftTypeName(swiftType: $0.value.swiftType, swiftModule: $0.value.swiftModule) }) + let groupedDictionary: [SwiftTypeName: [(JavaFullyQualifiedTypeName, SwiftTypeName)]] = Dictionary(grouping: translatedClasses, by: { + // SwiftTypeName(swiftType: $0.value.swiftType, swiftModule: $0.value.swiftModule) + $0.value + }) // Find all that are mapped to multiple names - let multipleClassesMappedToSameName: [SwiftTypeName: [(String, (String, String?))]] = groupedDictionary.filter { (key: SwiftTypeName, value: [(String, (String, String?))]) in + let multipleClassesMappedToSameName: [SwiftTypeName: [(JavaFullyQualifiedTypeName, SwiftTypeName)]] = groupedDictionary.filter { + (key: SwiftTypeName, value: [(JavaFullyQualifiedTypeName, SwiftTypeName)]) in value.count > 1 } diff --git a/Sources/SwiftJavaLib/JavaTranslator.swift b/Sources/SwiftJavaToolLib/JavaTranslator.swift similarity index 72% rename from Sources/SwiftJavaLib/JavaTranslator.swift rename to Sources/SwiftJavaToolLib/JavaTranslator.swift index 225d3868a..1c71afdae 100644 --- a/Sources/SwiftJavaLib/JavaTranslator.swift +++ b/Sources/SwiftJavaToolLib/JavaTranslator.swift @@ -12,17 +12,23 @@ // //===----------------------------------------------------------------------===// -import JavaKit -import JavaKitReflection +import SwiftJava +import JavaLangReflect import JavaTypes import SwiftBasicFormat import SwiftSyntax -import JavaKitConfigurationShared +import SwiftJavaConfigurationShared import SwiftSyntaxBuilder +import Foundation +import Logging /// Utility that translates Java classes into Swift source code to access /// those Java classes. package class JavaTranslator { + let config: Configuration + + let log: Logger + /// The name of the Swift module that we are translating into. let swiftModuleName: String @@ -35,7 +41,10 @@ package class JavaTranslator { /// A mapping from the name of each known Java class to the corresponding /// Swift type name and its Swift module. - package var translatedClasses: [String: (swiftType: String, swiftModule: String?)] = [:] + package var translatedClasses: [JavaFullyQualifiedTypeName: SwiftTypeName] = [ + "java.lang.Object": SwiftTypeName(module: "SwiftJava", name: "JavaObject"), + "byte[]": SwiftTypeName(module: nil, name: "[UInt8]") + ] /// A mapping from the name of each known Java class with the Swift value type /// (and its module) to which it is mapped. @@ -44,8 +53,8 @@ package class JavaTranslator { /// `translatedClasses` should map to a representation of the Java class (i.e., /// an AnyJavaObject-conforming type) whereas the entry here should map to /// a value type. - package let translatedToValueTypes: [String: (swiftType: String, swiftModule: String) ] = [ - "java.lang.String": ("String", "JavaKit"), + package let translatedToValueTypes: [JavaFullyQualifiedTypeName: SwiftTypeName] = [ + "java.lang.String": SwiftTypeName(module: "SwiftJava", name: "String"), ] /// The set of Swift modules that need to be imported to make the generated @@ -64,15 +73,21 @@ package class JavaTranslator { package var nestedClasses: [String: [JavaClass]] = [:] package init( + config: Configuration, swiftModuleName: String, environment: JNIEnvironment, translateAsClass: Bool = false, format: BasicFormat = JavaTranslator.defaultFormat ) { + self.config = config self.swiftModuleName = swiftModuleName self.environment = environment self.translateAsClass = translateAsClass self.format = format + + var l = Logger(label: "swift-java") + l.logLevel = .init(rawValue: (config.logLevel ?? .info).rawValue)! + self.log = l } /// Clear out any per-file state when we want to start a new file. @@ -93,8 +108,8 @@ extension JavaTranslator { /// Default set of modules that will always be imported. private static let defaultImportedSwiftModules: Set = [ - "JavaKit", - "JavaRuntime", + "SwiftJava", + "CSwiftJavaJNI", ] } @@ -112,8 +127,26 @@ extension JavaTranslator { // MARK: Type translation extension JavaTranslator { + + func getSwiftReturnTypeNameAsString( + method: JavaLangReflect.Method, + preferValueTypes: Bool, + outerOptional: OptionalKind + ) throws -> String { + let genericReturnType = method.getGenericReturnType() + + // Special handle the case when the return type is the generic type of the method: ` T foo()` + + return try getSwiftTypeNameAsString( + method: method, + genericReturnType!, + preferValueTypes: preferValueTypes, + outerOptional: outerOptional) + } + /// Turn a Java type into a string. func getSwiftTypeNameAsString( + method: JavaLangReflect.Method? = nil, _ javaType: Type, preferValueTypes: Bool, outerOptional: OptionalKind @@ -123,11 +156,7 @@ extension JavaTranslator { typeVariable.getBounds().count == 1, let bound = typeVariable.getBounds()[0] { - return try getSwiftTypeNameAsString( - bound, - preferValueTypes: preferValueTypes, - outerOptional: outerOptional - ) + return outerOptional.adjustTypeName(typeVariable.getName()) } // Replace wildcards with their upper bound. @@ -164,30 +193,42 @@ extension JavaTranslator { // Handle parameterized types by recursing on the raw type and the type // arguments. - if let parameterizedType = javaType.as(ParameterizedType.self), - let rawJavaType = parameterizedType.getRawType() - { - var rawSwiftType = try getSwiftTypeNameAsString( - rawJavaType, - preferValueTypes: false, - outerOptional: outerOptional - ) + if let parameterizedType = javaType.as(ParameterizedType.self) { + if let rawJavaType = parameterizedType.getRawType() { + var rawSwiftType = try getSwiftTypeNameAsString( + rawJavaType, + preferValueTypes: false, + outerOptional: outerOptional + ) - let optionalSuffix: String - if let lastChar = rawSwiftType.last, lastChar == "?" || lastChar == "!" { - optionalSuffix = "\(lastChar)" - rawSwiftType.removeLast() - } else { - optionalSuffix = "" - } + let optionalSuffix: String + if let lastChar = rawSwiftType.last, lastChar == "?" || lastChar == "!" { + optionalSuffix = "\(lastChar)" + rawSwiftType.removeLast() + } else { + optionalSuffix = "" + } - let typeArguments = try parameterizedType.getActualTypeArguments().compactMap { typeArg in - try typeArg.map { typeArg in - try getSwiftTypeNameAsString(typeArg, preferValueTypes: false, outerOptional: .nonoptional) + let typeArguments: [String] = try parameterizedType.getActualTypeArguments().compactMap { typeArg in + guard let typeArg else { return nil } + + let mappedSwiftName = try getSwiftTypeNameAsString(method: method, typeArg, preferValueTypes: false, outerOptional: .nonoptional) + + // FIXME: improve the get instead... + if mappedSwiftName == nil || mappedSwiftName == "JavaObject" { + // Try to salvage it, is it perhaps a type parameter? + if let method { + if method.getTypeParameters().contains(where: { $0?.getTypeName() == typeArg.getTypeName() }) { + return typeArg.getTypeName() + } + } + } + + return mappedSwiftName } - } - return "\(rawSwiftType)<\(typeArguments.joined(separator: ", "))>\(optionalSuffix)" + return "\(rawSwiftType)<\(typeArguments.joined(separator: ", "))>\(optionalSuffix)" + } } // Handle direct references to Java classes. @@ -196,10 +237,12 @@ extension JavaTranslator { } let (swiftName, isOptional) = try getSwiftTypeName(javaClass, preferValueTypes: preferValueTypes) - var resultString = swiftName - if isOptional { - resultString = outerOptional.adjustTypeName(resultString) - } + let resultString = + if isOptional { + outerOptional.adjustTypeName(swiftName) + } else { + swiftName + } return resultString } @@ -233,7 +276,10 @@ extension JavaTranslator { if preferValueTypes, let translatedValueType = translatedToValueTypes[name] { // Note that we need to import this Swift module. if translatedValueType.swiftModule != swiftModuleName { - importedSwiftModules.insert(translatedValueType.swiftModule) + guard let module = translatedValueType.swiftModule else { + preconditionFailure("Translated value type must have Swift module, but was nil! Type: \(translatedValueType)") + } + importedSwiftModules.insert(module) } return translatedValueType.swiftType diff --git a/Sources/SwiftJavaToolLib/JavaType+Equality.swift b/Sources/SwiftJavaToolLib/JavaType+Equality.swift new file mode 100644 index 000000000..b3d4a37a4 --- /dev/null +++ b/Sources/SwiftJavaToolLib/JavaType+Equality.swift @@ -0,0 +1,135 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftJava +import JavaLangReflect +import SwiftSyntax +import SwiftJavaConfigurationShared +import Logging + +extension Type { + /// Adjust the given type to use its bounds, mirroring what we do in + /// mapping Java types into Swift. + func adjustToJavaBounds(adjusted: inout Bool) -> Type { + if let typeVariable = self.as(TypeVariable.self), + typeVariable.getBounds().count == 1, + let bound = typeVariable.getBounds()[0] { + adjusted = true + return bound + } + + if let wildcardType = self.as(WildcardType.self), + wildcardType.getUpperBounds().count == 1, + let bound = wildcardType.getUpperBounds()[0] { + adjusted = true + return bound + } + + return self + } + + /// Determine whether this type is equivalent to or a subtype of the other + /// type. + func isEqualTo(_ other: Type, file: String = #file, line: Int = #line, function: String = #function) -> Bool { + if self.javaHolder.object == other.javaHolder.object { + return true + } + + // First, adjust types to their bounds, if we need to. + var anyAdjusted: Bool = false + let adjustedSelf = self.adjustToJavaBounds(adjusted: &anyAdjusted) + let adjustedOther = other.adjustToJavaBounds(adjusted: &anyAdjusted) + if anyAdjusted { + return adjustedSelf.isEqualTo(adjustedOther) + } + + // If both are classes, check for equivalence. + if let selfClass = self.as(JavaClass.self), + let otherClass = other.as(JavaClass.self) { + return selfClass.equals(otherClass.as(JavaObject.self)) + } + + // If both are arrays, check that their component types are equivalent. + if let selfArray = self.as(GenericArrayType.self), + let otherArray = other.as(GenericArrayType.self) { + return selfArray.getGenericComponentType().isEqualTo(otherArray.getGenericComponentType()) + } + + // If both are parameterized types, check their raw type and type + // arguments for equivalence. + if let selfParameterizedType = self.as(ParameterizedType.self), + let otherParameterizedType = other.as(ParameterizedType.self) { + if !selfParameterizedType.getRawType().isEqualTo(otherParameterizedType.getRawType()) { + return false + } + + return selfParameterizedType.getActualTypeArguments() + .allTypesEqual(otherParameterizedType.getActualTypeArguments()) + } + + // If both are type variables, compare their bounds. + // FIXME: This is a hack. + if let selfTypeVariable = self.as(TypeVariable.self), + let otherTypeVariable = other.as(TypeVariable.self) { + return selfTypeVariable.getBounds().allTypesEqual(otherTypeVariable.getBounds()) + } + + // If both are wildcards, compare their upper and lower bounds. + if let selfWildcard = self.as(WildcardType.self), + let otherWildcard = other.as(WildcardType.self) { + return selfWildcard.getUpperBounds().allTypesEqual(otherWildcard.getUpperBounds()) + && selfWildcard.getLowerBounds().allTypesEqual(otherWildcard.getLowerBounds()) + } + + return false + } + + /// Determine whether this type is equivalent to or a subtype of the + /// other type. + func isEqualToOrSubtypeOf(_ other: Type) -> Bool { + // First, adjust types to their bounds, if we need to. + var anyAdjusted: Bool = false + let adjustedSelf = self.adjustToJavaBounds(adjusted: &anyAdjusted) + let adjustedOther = other.adjustToJavaBounds(adjusted: &anyAdjusted) + if anyAdjusted { + return adjustedSelf.isEqualToOrSubtypeOf(adjustedOther) + } + + if isEqualTo(other) { + return true + } + + // If both are classes, check for subclassing. + if let selfClass = self.as(JavaClass.self), + let otherClass = other.as(JavaClass.self) { + // If either is a Java array, then this cannot be a subtype relationship + // in Swift. + if selfClass.isArray() || otherClass.isArray() { + return false + } + + return selfClass.isSubclass(of: otherClass) + } + + // Anything object-like is a subclass of java.lang.Object + if let otherClass = other.as(JavaClass.self), + otherClass.getName() == "java.lang.Object" { + if self.is(GenericArrayType.self) || self.is(ParameterizedType.self) || + self.is(WildcardType.self) || self.is(TypeVariable.self) { + return true + } + } + return false + } +} diff --git a/Sources/SwiftJavaLib/MethodVariance.swift b/Sources/SwiftJavaToolLib/MethodVariance.swift similarity index 98% rename from Sources/SwiftJavaLib/MethodVariance.swift rename to Sources/SwiftJavaToolLib/MethodVariance.swift index 09bd64442..c130a75c9 100644 --- a/Sources/SwiftJavaLib/MethodVariance.swift +++ b/Sources/SwiftJavaToolLib/MethodVariance.swift @@ -11,8 +11,8 @@ // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// -import JavaKit -import JavaKitReflection +import SwiftJava +import JavaLangReflect /// Captures the relationship between two methods by comparing their parameter /// and result types. diff --git a/Sources/SwiftJavaLib/OptionalKind.swift b/Sources/SwiftJavaToolLib/OptionalKind.swift similarity index 100% rename from Sources/SwiftJavaLib/OptionalKind.swift rename to Sources/SwiftJavaToolLib/OptionalKind.swift diff --git a/Sources/SwiftJavaLib/StringExtras.swift b/Sources/SwiftJavaToolLib/StringExtras.swift similarity index 93% rename from Sources/SwiftJavaLib/StringExtras.swift rename to Sources/SwiftJavaToolLib/StringExtras.swift index e69f379c3..c3ac5390f 100644 --- a/Sources/SwiftJavaLib/StringExtras.swift +++ b/Sources/SwiftJavaToolLib/StringExtras.swift @@ -35,6 +35,11 @@ extension String { return "`\(self)`" } + /// Returns whether this is a valid Swift function name + var isValidSwiftFunctionName: Bool { + !self.starts(with: "$") + } + /// Replace all occurrences of one character in the string with another. public func replacing(_ character: Character, with replacement: Character) -> String { return replacingOccurrences(of: String(character), with: String(replacement)) diff --git a/Sources/SwiftJavaLib/TranslationError.swift b/Sources/SwiftJavaToolLib/TranslationError.swift similarity index 97% rename from Sources/SwiftJavaLib/TranslationError.swift rename to Sources/SwiftJavaToolLib/TranslationError.swift index d44fd2d7d..1e01134b0 100644 --- a/Sources/SwiftJavaLib/TranslationError.swift +++ b/Sources/SwiftJavaToolLib/TranslationError.swift @@ -12,7 +12,7 @@ // //===----------------------------------------------------------------------===// -import JavaKitReflection +import JavaLangReflect /// Errors that can occur when translating Java types into Swift. enum TranslationError: Error { diff --git a/Sources/SwiftKitSwift/SwiftKit.swift b/Sources/SwiftRuntimeFunctions/SwiftRuntimeFunctions.swift similarity index 97% rename from Sources/SwiftKitSwift/SwiftKit.swift rename to Sources/SwiftRuntimeFunctions/SwiftRuntimeFunctions.swift index 38b3c1c88..fb9781630 100644 --- a/Sources/SwiftKitSwift/SwiftKit.swift +++ b/Sources/SwiftRuntimeFunctions/SwiftRuntimeFunctions.swift @@ -34,7 +34,7 @@ public func _swiftjava_swift_isUniquelyReferenced(object: UnsafeMutableRawPointe @_alwaysEmitIntoClient @_transparent - internal func _swiftjava_withHeapObject( +func _swiftjava_withHeapObject( of object: AnyObject, _ body: (UnsafeMutableRawPointer) -> R ) -> R { diff --git a/Sources/_CShims/process_shims.c b/Sources/_CShims/process_shims.c deleted file mode 100644 index fe96c675c..000000000 --- a/Sources/_CShims/process_shims.c +++ /dev/null @@ -1,340 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift.org project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift.org project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -#include "include/_CShimsTargetConditionals.h" -#include "include/process_shims.h" - -#if !TARGET_OS_WINDOWS -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -int _was_process_exited(int status) { - return WIFEXITED(status); -} - -int _get_exit_code(int status) { - return WEXITSTATUS(status); -} - -int _was_process_signaled(int status) { - return WIFSIGNALED(status); -} - -int _get_signal_code(int status) { - return WTERMSIG(status); -} - -int _was_process_suspended(int status) { - return WIFSTOPPED(status); -} - -#if TARGET_OS_LINUX -#include - -int _shims_snprintf( - char * _Nonnull str, - int len, - const char * _Nonnull format, - char * _Nonnull str1, - char * _Nonnull str2 -) { - return snprintf(str, len, format, str1, str2); -} -#endif - -// MARK: - Darwin (posix_spawn) -#if TARGET_OS_MAC - -int _subprocess_spawn( - pid_t * _Nonnull pid, - const char * _Nonnull exec_path, - const posix_spawn_file_actions_t _Nullable * _Nonnull file_actions, - const posix_spawnattr_t _Nullable * _Nonnull spawn_attrs, - char * _Nullable const args[_Nonnull], - char * _Nullable const env[_Nullable], - uid_t * _Nullable uid, - gid_t * _Nullable gid, - int number_of_sgroups, const gid_t * _Nullable sgroups, - int create_session -) { - int require_pre_fork = uid != NULL || - gid != NULL || - number_of_sgroups > 0 || - create_session > 0; - - if (require_pre_fork != 0) { -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated" - pid_t childPid = vfork(); -#pragma GCC diagnostic pop - if (childPid != 0) { - *pid = childPid; - return childPid < 0 ? errno : 0; - } - - if (number_of_sgroups > 0 && sgroups != NULL) { - if (setgroups(number_of_sgroups, sgroups) != 0) { - return errno; - } - } - - if (uid != NULL) { - if (setuid(*uid) != 0) { - return errno; - } - } - - if (gid != NULL) { - if (setgid(*gid) != 0) { - return errno; - } - } - - if (create_session != 0) { - (void)setsid(); - } - } - - // Set POSIX_SPAWN_SETEXEC if we already forked - if (require_pre_fork) { - short flags = 0; - int rc = posix_spawnattr_getflags(spawn_attrs, &flags); - if (rc != 0) { - return rc; - } - - rc = posix_spawnattr_setflags( - (posix_spawnattr_t *)spawn_attrs, flags | POSIX_SPAWN_SETEXEC - ); - if (rc != 0) { - return rc; - } - } - - // Spawn - return posix_spawn(pid, exec_path, file_actions, spawn_attrs, args, env); -} - -#endif // TARGET_OS_MAC - -// MARK: - Linux (fork/exec + posix_spawn fallback) - -#if _POSIX_SPAWN -static int _subprocess_posix_spawn_fallback( - pid_t * _Nonnull pid, - const char * _Nonnull exec_path, - const char * _Nullable working_directory, - const int file_descriptors[_Nonnull], - char * _Nullable const args[_Nonnull], - char * _Nullable const env[_Nullable], - gid_t * _Nullable process_group_id -) { - // Setup stdin, stdout, and stderr - posix_spawn_file_actions_t file_actions; - - int rc = posix_spawn_file_actions_init(&file_actions); - if (rc != 0) { return rc; } - if (file_descriptors[0] >= 0) { - rc = posix_spawn_file_actions_adddup2( - &file_actions, file_descriptors[0], STDIN_FILENO - ); - if (rc != 0) { return rc; } - } - if (file_descriptors[2] >= 0) { - rc = posix_spawn_file_actions_adddup2( - &file_actions, file_descriptors[2], STDOUT_FILENO - ); - if (rc != 0) { return rc; } - } - if (file_descriptors[4] >= 0) { - rc = posix_spawn_file_actions_adddup2( - &file_actions, file_descriptors[4], STDERR_FILENO - ); - if (rc != 0) { return rc; } - } - - // Close parent side - if (file_descriptors[1] >= 0) { - rc = posix_spawn_file_actions_addclose(&file_actions, file_descriptors[1]); - if (rc != 0) { return rc; } - } - if (file_descriptors[3] >= 0) { - rc = posix_spawn_file_actions_addclose(&file_actions, file_descriptors[3]); - if (rc != 0) { return rc; } - } - if (file_descriptors[5] >= 0) { - rc = posix_spawn_file_actions_addclose(&file_actions, file_descriptors[5]); - if (rc != 0) { return rc; } - } - - // Setup spawnattr - posix_spawnattr_t spawn_attr; - rc = posix_spawnattr_init(&spawn_attr); - if (rc != 0) { return rc; } - // Masks - sigset_t no_signals; - sigset_t all_signals; - sigemptyset(&no_signals); - sigfillset(&all_signals); - rc = posix_spawnattr_setsigmask(&spawn_attr, &no_signals); - if (rc != 0) { return rc; } - rc = posix_spawnattr_setsigdefault(&spawn_attr, &all_signals); - if (rc != 0) { return rc; } - // Flags - short flags = POSIX_SPAWN_SETSIGMASK | POSIX_SPAWN_SETSIGDEF; - if (process_group_id != NULL) { - flags |= POSIX_SPAWN_SETPGROUP; - rc = posix_spawnattr_setpgroup(&spawn_attr, *process_group_id); - if (rc != 0) { return rc; } - } - rc = posix_spawnattr_setflags(&spawn_attr, flags); - - // Spawn! - rc = posix_spawn( - pid, exec_path, - &file_actions, &spawn_attr, - args, env - ); - posix_spawn_file_actions_destroy(&file_actions); - posix_spawnattr_destroy(&spawn_attr); - return rc; -} -#endif // _POSIX_SPAWN - -int _subprocess_fork_exec( - pid_t * _Nonnull pid, - const char * _Nonnull exec_path, - const char * _Nullable working_directory, - const int file_descriptors[_Nonnull], - char * _Nullable const args[_Nonnull], - char * _Nullable const env[_Nullable], - uid_t * _Nullable uid, - gid_t * _Nullable gid, - gid_t * _Nullable process_group_id, - int number_of_sgroups, const gid_t * _Nullable sgroups, - int create_session, - void (* _Nullable configurator)(void) -) { - int require_pre_fork = working_directory != NULL || - uid != NULL || - gid != NULL || - process_group_id != NULL || - (number_of_sgroups > 0 && sgroups != NULL) || - create_session || - configurator != NULL; - -#if _POSIX_SPAWN - // If posix_spawn is available on this platform and - // we do not require prefork, use posix_spawn if possible. - // - // (Glibc's posix_spawn does not support - // `POSIX_SPAWN_SETEXEC` therefore we have to keep - // using fork/exec if `require_pre_fork` is true. - if (require_pre_fork == 0) { - return _subprocess_posix_spawn_fallback( - pid, exec_path, - working_directory, - file_descriptors, - args, env, - process_group_id - ); - } -#endif - - pid_t child_pid = fork(); - if (child_pid != 0) { - *pid = child_pid; - return child_pid < 0 ? errno : 0; - } - - if (working_directory != NULL) { - if (chdir(working_directory) != 0) { - return errno; - } - } - - - if (uid != NULL) { - if (setuid(*uid) != 0) { - return errno; - } - } - - if (gid != NULL) { - if (setgid(*gid) != 0) { - return errno; - } - } - - if (number_of_sgroups > 0 && sgroups != NULL) { - if (setgroups(number_of_sgroups, sgroups) != 0) { - return errno; - } - } - - if (create_session != 0) { - (void)setsid(); - } - - if (process_group_id != NULL) { - (void)setpgid(0, *process_group_id); - } - - // Bind stdin, stdout, and stderr - int rc = 0; - if (file_descriptors[0] >= 0) { - rc = dup2(file_descriptors[0], STDIN_FILENO); - if (rc < 0) { return errno; } - } - if (file_descriptors[2] >= 0) { - rc = dup2(file_descriptors[2], STDOUT_FILENO); - if (rc < 0) { return errno; } - } - if (file_descriptors[4] >= 0) { - rc = dup2(file_descriptors[4], STDERR_FILENO); - if (rc < 0) { return errno; } - } - // Close parent side - if (file_descriptors[1] >= 0) { - rc = close(file_descriptors[1]); - } - if (file_descriptors[3] >= 0) { - rc = close(file_descriptors[3]); - } - if (file_descriptors[4] >= 0) { - rc = close(file_descriptors[5]); - } - if (rc != 0) { - return errno; - } - // Run custom configuratior - if (configurator != NULL) { - configurator(); - } - // Finally, exec - execve(exec_path, args, env); - // If we got here, something went wrong - return errno; -} - -#endif // !TARGET_OS_WINDOWS - diff --git a/Sources/_Subprocess/API.swift b/Sources/_Subprocess/API.swift new file mode 100644 index 000000000..9673d3e11 --- /dev/null +++ b/Sources/_Subprocess/API.swift @@ -0,0 +1,370 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +#if canImport(System) +import System +#else +@preconcurrency import SystemPackage +#endif + +// MARK: - Collected Result + +/// Run a executable with given parameters asynchrously and returns +/// a `CollectedResult` containing the output of the child process. +/// - Parameters: +/// - executable: The executable to run. +/// - arguments: The arguments to pass to the executable. +/// - environment: The environment in which to run the executable. +/// - workingDirectory: The working directory in which to run the executable. +/// - platformOptions: The platform specific options to use +/// when running the executable. +/// - input: The input to send to the executable. +/// - output: The method to use for redirecting the standard output. +/// - error: The method to use for redirecting the standard error. +/// - Returns a CollectedResult containing the result of the run. +@available(macOS 15.0, *) // FIXME: manually added availability +public func run< + Input: InputProtocol, + Output: OutputProtocol, + Error: OutputProtocol +>( + _ executable: Executable, + arguments: Arguments = [], + environment: Environment = .inherit, + workingDirectory: FilePath? = nil, + platformOptions: PlatformOptions = PlatformOptions(), + input: Input = .none, + output: Output = .string, + error: Error = .discarded +) async throws -> CollectedResult { + let configuration = Configuration( + executable: executable, + arguments: arguments, + environment: environment, + workingDirectory: workingDirectory, + platformOptions: platformOptions + ) + return try await run( + configuration, + input: input, + output: output, + error: error + ) +} + +// MARK: - Custom Execution Body + +/// Run a executable with given parameters and a custom closure +/// to manage the running subprocess' lifetime and its IOs. +/// - Parameters: +/// - executable: The executable to run. +/// - arguments: The arguments to pass to the executable. +/// - environment: The environment in which to run the executable. +/// - workingDirectory: The working directory in which to run the executable. +/// - platformOptions: The platform specific options to use +/// when running the executable. +/// - input: The input to send to the executable. +/// - output: How to manage the executable standard ouput. +/// - error: How to manager executable standard error. +/// - isolation: the isolation context to run the body closure. +/// - body: The custom execution body to manually control the running process +/// - Returns a ExecutableResult type containing the return value +/// of the closure. +@available(macOS 15.0, *) // FIXME: manually added availability +public func run( + _ executable: Executable, + arguments: Arguments = [], + environment: Environment = .inherit, + workingDirectory: FilePath? = nil, + platformOptions: PlatformOptions = PlatformOptions(), + input: Input = .none, + output: Output, + error: Error, + isolation: isolated (any Actor)? = #isolation, + body: ((Execution) async throws -> Result) +) async throws -> ExecutionResult where Output.OutputType == Void, Error.OutputType == Void { + return try await Configuration( + executable: executable, + arguments: arguments, + environment: environment, + workingDirectory: workingDirectory, + platformOptions: platformOptions + ) + .run(input: input, output: output, error: error, body) +} + +/// Run a executable with given parameters and a custom closure +/// to manage the running subprocess' lifetime and write to its +/// standard input via `StandardInputWriter` +/// - Parameters: +/// - executable: The executable to run. +/// - arguments: The arguments to pass to the executable. +/// - environment: The environment in which to run the executable. +/// - workingDirectory: The working directory in which to run the executable. +/// - platformOptions: The platform specific options to use +/// when running the executable. +/// - output:How to handle executable's standard output +/// - error: How to handle executable's standard error +/// - isolation: the isolation context to run the body closure. +/// - body: The custom execution body to manually control the running process +/// - Returns a ExecutableResult type containing the return value +/// of the closure. +@available(macOS 15.0, *) // FIXME: manually added availability +public func run( + _ executable: Executable, + arguments: Arguments = [], + environment: Environment = .inherit, + workingDirectory: FilePath? = nil, + platformOptions: PlatformOptions = PlatformOptions(), + output: Output, + error: Error, + isolation: isolated (any Actor)? = #isolation, + body: ((Execution, StandardInputWriter) async throws -> Result) +) async throws -> ExecutionResult where Output.OutputType == Void, Error.OutputType == Void { + return try await Configuration( + executable: executable, + arguments: arguments, + environment: environment, + workingDirectory: workingDirectory, + platformOptions: platformOptions + ) + .run(output: output, error: error, body) +} + +// MARK: - Configuration Based + +/// Run a `Configuration` asynchrously and returns +/// a `CollectedResult` containing the output of the child process. +/// - Parameters: +/// - configuration: The `Subprocess` configuration to run. +/// - input: The input to send to the executable. +/// - output: The method to use for redirecting the standard output. +/// - error: The method to use for redirecting the standard error. +/// - Returns a CollectedResult containing the result of the run. +@available(macOS 15.0, *) // FIXME: manually added availability +public func run< + Input: InputProtocol, + Output: OutputProtocol, + Error: OutputProtocol +>( + _ configuration: Configuration, + input: Input = .none, + output: Output = .string, + error: Error = .discarded +) async throws -> CollectedResult { + let result = try await configuration.run( + input: input, + output: output, + error: error + ) { execution in + let ( + standardOutput, + standardError + ) = try await execution.captureIOs() + return ( + processIdentifier: execution.processIdentifier, + standardOutput: standardOutput, + standardError: standardError + ) + } + return CollectedResult( + processIdentifier: result.value.processIdentifier, + terminationStatus: result.terminationStatus, + standardOutput: result.value.standardOutput, + standardError: result.value.standardError + ) +} + +/// Run a executable with given parameters specified by a `Configuration` +/// - Parameters: +/// - configuration: The `Subprocess` configuration to run. +/// - output: The method to use for redirecting the standard output. +/// - error: The method to use for redirecting the standard error. +/// - isolation: the isolation context to run the body closure. +/// - body: The custom configuration body to manually control +/// the running process and write to its standard input. +/// - Returns a ExecutableResult type containing the return value +/// of the closure. +@available(macOS 15.0, *) // FIXME: manually added availability +public func run( + _ configuration: Configuration, + output: Output, + error: Error, + isolation: isolated (any Actor)? = #isolation, + body: ((Execution, StandardInputWriter) async throws -> Result) +) async throws -> ExecutionResult where Output.OutputType == Void, Error.OutputType == Void { + return try await configuration.run(output: output, error: error, body) +} + +// MARK: - Detached + +/// Run a executable with given parameters and return its process +/// identifier immediately without monitoring the state of the +/// subprocess nor waiting until it exits. +/// +/// This method is useful for launching subprocesses that outlive their +/// parents (for example, daemons and trampolines). +/// +/// - Parameters: +/// - executable: The executable to run. +/// - arguments: The arguments to pass to the executable. +/// - environment: The environment to use for the process. +/// - workingDirectory: The working directory for the process. +/// - platformOptions: The platform specific options to use for the process. +/// - input: A file descriptor to bind to the subprocess' standard input. +/// - output: A file descriptor to bind to the subprocess' standard output. +/// - error: A file descriptor to bind to the subprocess' standard error. +/// - Returns: the process identifier for the subprocess. +@available(macOS 15.0, *) // FIXME: manually added availability +public func runDetached( + _ executable: Executable, + arguments: Arguments = [], + environment: Environment = .inherit, + workingDirectory: FilePath? = nil, + platformOptions: PlatformOptions = PlatformOptions(), + input: FileDescriptor? = nil, + output: FileDescriptor? = nil, + error: FileDescriptor? = nil +) throws -> ProcessIdentifier { + let config: Configuration = Configuration( + executable: executable, + arguments: arguments, + environment: environment, + workingDirectory: workingDirectory, + platformOptions: platformOptions + ) + return try runDetached(config, input: input, output: output, error: error) +} + +/// Run a executable with given configuration and return its process +/// identifier immediately without monitoring the state of the +/// subprocess nor waiting until it exits. +/// +/// This method is useful for launching subprocesses that outlive their +/// parents (for example, daemons and trampolines). +/// +/// - Parameters: +/// - configuration: The `Subprocess` configuration to run. +/// - input: A file descriptor to bind to the subprocess' standard input. +/// - output: A file descriptor to bind to the subprocess' standard output. +/// - error: A file descriptor to bind to the subprocess' standard error. +/// - Returns: the process identifier for the subprocess. +@available(macOS 15.0, *) // FIXME: manually added availability +public func runDetached( + _ configuration: Configuration, + input: FileDescriptor? = nil, + output: FileDescriptor? = nil, + error: FileDescriptor? = nil +) throws -> ProcessIdentifier { + switch (input, output, error) { + case (.none, .none, .none): + let processOutput = DiscardedOutput() + let processError = DiscardedOutput() + return try configuration.spawn( + withInput: NoInput().createPipe(), + output: processOutput, + outputPipe: try processOutput.createPipe(), + error: processError, + errorPipe: try processError.createPipe() + ).processIdentifier + case (.none, .none, .some(let errorFd)): + let processOutput = DiscardedOutput() + let processError = FileDescriptorOutput(fileDescriptor: errorFd, closeAfterSpawningProcess: false) + return try configuration.spawn( + withInput: NoInput().createPipe(), + output: processOutput, + outputPipe: try processOutput.createPipe(), + error: processError, + errorPipe: try processError.createPipe() + ).processIdentifier + case (.none, .some(let outputFd), .none): + let processOutput = FileDescriptorOutput(fileDescriptor: outputFd, closeAfterSpawningProcess: false) + let processError = DiscardedOutput() + return try configuration.spawn( + withInput: NoInput().createPipe(), + output: processOutput, + outputPipe: try processOutput.createPipe(), + error: processError, + errorPipe: try processError.createPipe() + ).processIdentifier + case (.none, .some(let outputFd), .some(let errorFd)): + let processOutput = FileDescriptorOutput( + fileDescriptor: outputFd, + closeAfterSpawningProcess: false + ) + let processError = FileDescriptorOutput( + fileDescriptor: errorFd, + closeAfterSpawningProcess: false + ) + return try configuration.spawn( + withInput: NoInput().createPipe(), + output: processOutput, + outputPipe: try processOutput.createPipe(), + error: processError, + errorPipe: try processError.createPipe() + ).processIdentifier + case (.some(let inputFd), .none, .none): + let processOutput = DiscardedOutput() + let processError = DiscardedOutput() + return try configuration.spawn( + withInput: FileDescriptorInput( + fileDescriptor: inputFd, + closeAfterSpawningProcess: false + ).createPipe(), + output: processOutput, + outputPipe: try processOutput.createPipe(), + error: processError, + errorPipe: try processError.createPipe() + ).processIdentifier + case (.some(let inputFd), .none, .some(let errorFd)): + let processOutput = DiscardedOutput() + let processError = FileDescriptorOutput( + fileDescriptor: errorFd, + closeAfterSpawningProcess: false + ) + return try configuration.spawn( + withInput: FileDescriptorInput(fileDescriptor: inputFd, closeAfterSpawningProcess: false).createPipe(), + output: processOutput, + outputPipe: try processOutput.createPipe(), + error: processError, + errorPipe: try processError.createPipe() + ).processIdentifier + case (.some(let inputFd), .some(let outputFd), .none): + let processOutput = FileDescriptorOutput( + fileDescriptor: outputFd, + closeAfterSpawningProcess: false + ) + let processError = DiscardedOutput() + return try configuration.spawn( + withInput: FileDescriptorInput(fileDescriptor: inputFd, closeAfterSpawningProcess: false).createPipe(), + output: processOutput, + outputPipe: try processOutput.createPipe(), + error: processError, + errorPipe: try processError.createPipe() + ).processIdentifier + case (.some(let inputFd), .some(let outputFd), .some(let errorFd)): + let processOutput = FileDescriptorOutput( + fileDescriptor: outputFd, + closeAfterSpawningProcess: false + ) + let processError = FileDescriptorOutput( + fileDescriptor: errorFd, + closeAfterSpawningProcess: false + ) + return try configuration.spawn( + withInput: FileDescriptorInput(fileDescriptor: inputFd, closeAfterSpawningProcess: false).createPipe(), + output: processOutput, + outputPipe: try processOutput.createPipe(), + error: processError, + errorPipe: try processError.createPipe() + ).processIdentifier + } +} diff --git a/Sources/_Subprocess/AsyncBufferSequence.swift b/Sources/_Subprocess/AsyncBufferSequence.swift new file mode 100644 index 000000000..4316f7ebb --- /dev/null +++ b/Sources/_Subprocess/AsyncBufferSequence.swift @@ -0,0 +1,99 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +#if canImport(System) +import System +#else +@preconcurrency import SystemPackage +#endif + +internal struct AsyncBufferSequence: AsyncSequence, Sendable { + internal typealias Failure = any Swift.Error + + internal typealias Element = SequenceOutput.Buffer + + @_nonSendable + internal struct Iterator: AsyncIteratorProtocol { + internal typealias Element = SequenceOutput.Buffer + + private let fileDescriptor: TrackedFileDescriptor + private var buffer: [UInt8] + private var currentPosition: Int + private var finished: Bool + + internal init(fileDescriptor: TrackedFileDescriptor) { + self.fileDescriptor = fileDescriptor + self.buffer = [] + self.currentPosition = 0 + self.finished = false + } + + internal mutating func next() async throws -> SequenceOutput.Buffer? { + let data = try await self.fileDescriptor.wrapped.readChunk( + upToLength: readBufferSize + ) + if data == nil { + // We finished reading. Close the file descriptor now + try self.fileDescriptor.safelyClose() + return nil + } + return data + } + } + + private let fileDescriptor: TrackedFileDescriptor + + init(fileDescriptor: TrackedFileDescriptor) { + self.fileDescriptor = fileDescriptor + } + + internal func makeAsyncIterator() -> Iterator { + return Iterator(fileDescriptor: self.fileDescriptor) + } +} + +// MARK: - Page Size +import _SubprocessCShims + +#if canImport(Darwin) +import Darwin +internal import MachO.dyld + +private let _pageSize: Int = { + Int(_subprocess_vm_size()) +}() +#elseif canImport(WinSDK) +import WinSDK +private let _pageSize: Int = { + var sysInfo: SYSTEM_INFO = SYSTEM_INFO() + GetSystemInfo(&sysInfo) + return Int(sysInfo.dwPageSize) +}() +#elseif os(WASI) +// WebAssembly defines a fixed page size +private let _pageSize: Int = 65_536 +#elseif canImport(Android) +@preconcurrency import Android +private let _pageSize: Int = Int(getpagesize()) +#elseif canImport(Glibc) +@preconcurrency import Glibc +private let _pageSize: Int = Int(getpagesize()) +#elseif canImport(Musl) +@preconcurrency import Musl +private let _pageSize: Int = Int(getpagesize()) +#elseif canImport(C) +private let _pageSize: Int = Int(getpagesize()) +#endif // canImport(Darwin) + +@inline(__always) +internal var readBufferSize: Int { + return _pageSize +} diff --git a/Sources/_Subprocess/Buffer.swift b/Sources/_Subprocess/Buffer.swift new file mode 100644 index 000000000..3ce73d7ea --- /dev/null +++ b/Sources/_Subprocess/Buffer.swift @@ -0,0 +1,104 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +@preconcurrency internal import Dispatch + +extension SequenceOutput { + /// A immutable collection of bytes + public struct Buffer: Sendable { + #if os(Windows) + private var data: [UInt8] + + internal init(data: [UInt8]) { + self.data = data + } + #else + private var data: DispatchData + + internal init(data: DispatchData) { + self.data = data + } + #endif + } +} + +// MARK: - Properties +extension SequenceOutput.Buffer { + /// Number of bytes stored in the buffer + public var count: Int { + return self.data.count + } + + /// A Boolean value indicating whether the collection is empty. + public var isEmpty: Bool { + return self.data.isEmpty + } +} + +// MARK: - Accessors +extension SequenceOutput.Buffer { + #if !SubprocessSpan + /// Access the raw bytes stored in this buffer + /// - Parameter body: A closure with an `UnsafeRawBufferPointer` parameter that + /// points to the contiguous storage for the type. If no such storage exists, + /// the method creates it. If body has a return value, this method also returns + /// that value. The argument is valid only for the duration of the + /// closure’s SequenceOutput. + /// - Returns: The return value, if any, of the body closure parameter. + public func withUnsafeBytes( + _ body: (UnsafeRawBufferPointer) throws -> ResultType + ) rethrows -> ResultType { + return try self._withUnsafeBytes(body) + } + #endif // !SubprocessSpan + + internal func _withUnsafeBytes( + _ body: (UnsafeRawBufferPointer) throws -> ResultType + ) rethrows -> ResultType { + #if os(Windows) + return try self.data.withUnsafeBytes(body) + #else + // Although DispatchData was designed to be uncontiguous, in practice + // we found that almost all DispatchData are contiguous. Therefore + // we can access this body in O(1) most of the time. + return try self.data.withUnsafeBytes { ptr in + let bytes = UnsafeRawBufferPointer(start: ptr, count: self.data.count) + return try body(bytes) + } + #endif + } + + private enum SpanBacking { + case pointer(UnsafeBufferPointer) + case array([UInt8]) + } +} + +// MARK: - Hashable, Equatable +extension SequenceOutput.Buffer: Equatable, Hashable { + #if os(Windows) + // Compiler generated conformances + #else + public static func == (lhs: SequenceOutput.Buffer, rhs: SequenceOutput.Buffer) -> Bool { + return lhs.data.elementsEqual(rhs.data) + } + + public func hash(into hasher: inout Hasher) { + self.data.withUnsafeBytes { ptr in + let bytes = UnsafeRawBufferPointer( + start: ptr, + count: self.data.count + ) + hasher.combine(bytes: bytes) + } + } + #endif +} diff --git a/Sources/_Subprocess/Configuration.swift b/Sources/_Subprocess/Configuration.swift new file mode 100644 index 000000000..ba6f15bab --- /dev/null +++ b/Sources/_Subprocess/Configuration.swift @@ -0,0 +1,851 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +#if canImport(System) +import System +#else +@preconcurrency import SystemPackage +#endif + +#if canImport(Darwin) +import Darwin +#elseif canImport(Bionic) +import Bionic +#elseif canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#elseif canImport(WinSDK) +import WinSDK +#endif + +internal import Dispatch + +/// A collection of configurations parameters to use when +/// spawning a subprocess. +public struct Configuration: Sendable { + /// The executable to run. + public var executable: Executable + /// The arguments to pass to the executable. + public var arguments: Arguments + /// The environment to use when running the executable. + public var environment: Environment + /// The working directory to use when running the executable. + public var workingDirectory: FilePath + /// The platform specific options to use when + /// running the subprocess. + public var platformOptions: PlatformOptions + + public init( + executable: Executable, + arguments: Arguments = [], + environment: Environment = .inherit, + workingDirectory: FilePath? = nil, + platformOptions: PlatformOptions = PlatformOptions() + ) { + self.executable = executable + self.arguments = arguments + self.environment = environment + self.workingDirectory = workingDirectory ?? .currentWorkingDirectory + self.platformOptions = platformOptions + } + + @available(macOS 15.0, *) // FIXME: manually added availability + internal func run< + Result, + Output: OutputProtocol, + Error: OutputProtocol + >( + output: Output, + error: Error, + isolation: isolated (any Actor)? = #isolation, + _ body: ( + Execution, + StandardInputWriter + ) async throws -> Result + ) async throws -> ExecutionResult { + let input = CustomWriteInput() + + let inputPipe = try input.createPipe() + let outputPipe = try output.createPipe() + let errorPipe = try error.createPipe() + + let execution = try self.spawn( + withInput: inputPipe, + output: output, + outputPipe: outputPipe, + error: error, + errorPipe: errorPipe + ) + // After spawn, cleanup child side fds + try await self.cleanup( + execution: execution, + inputPipe: inputPipe, + outputPipe: outputPipe, + errorPipe: errorPipe, + childSide: true, + parentSide: false, + attemptToTerminateSubProcess: false + ) + return try await withAsyncTaskCleanupHandler { + async let waitingStatus = try await monitorProcessTermination( + forProcessWithIdentifier: execution.processIdentifier + ) + // Body runs in the same isolation + let result = try await body( + execution, + .init(fileDescriptor: inputPipe.writeFileDescriptor!) + ) + return ExecutionResult( + terminationStatus: try await waitingStatus, + value: result + ) + } onCleanup: { + // Attempt to terminate the child process + // Since the task has already been cancelled, + // this is the best we can do + try? await self.cleanup( + execution: execution, + inputPipe: inputPipe, + outputPipe: outputPipe, + errorPipe: errorPipe, + childSide: false, + parentSide: true, + attemptToTerminateSubProcess: true + ) + } + } + + @available(macOS 15.0, *) // FIXME: manually added availability + internal func run< + Result, + Input: InputProtocol, + Output: OutputProtocol, + Error: OutputProtocol + >( + input: Input, + output: Output, + error: Error, + isolation: isolated (any Actor)? = #isolation, + _ body: ((Execution) async throws -> Result) + ) async throws -> ExecutionResult { + + let inputPipe = try input.createPipe() + let outputPipe = try output.createPipe() + let errorPipe = try error.createPipe() + + let execution = try self.spawn( + withInput: inputPipe, + output: output, + outputPipe: outputPipe, + error: error, + errorPipe: errorPipe + ) + // After spawn, clean up child side + try await self.cleanup( + execution: execution, + inputPipe: inputPipe, + outputPipe: outputPipe, + errorPipe: errorPipe, + childSide: true, + parentSide: false, + attemptToTerminateSubProcess: false + ) + + return try await withAsyncTaskCleanupHandler { + return try await withThrowingTaskGroup( + of: TerminationStatus?.self, + returning: ExecutionResult.self + ) { group in + group.addTask { + if let writeFd = inputPipe.writeFileDescriptor { + let writer = StandardInputWriter(fileDescriptor: writeFd) + try await input.write(with: writer) + try await writer.finish() + } + return nil + } + group.addTask { + return try await monitorProcessTermination( + forProcessWithIdentifier: execution.processIdentifier + ) + } + + // Body runs in the same isolation + let result = try await body(execution) + var status: TerminationStatus? = nil + while let monitorResult = try await group.next() { + if let monitorResult = monitorResult { + status = monitorResult + } + } + return ExecutionResult(terminationStatus: status!, value: result) + } + } onCleanup: { + // Attempt to terminate the child process + // Since the task has already been cancelled, + // this is the best we can do + try? await self.cleanup( + execution: execution, + inputPipe: inputPipe, + outputPipe: outputPipe, + errorPipe: errorPipe, + childSide: false, + parentSide: true, + attemptToTerminateSubProcess: true + ) + } + } +} + +extension Configuration: CustomStringConvertible, CustomDebugStringConvertible { + public var description: String { + return """ + Configuration( + executable: \(self.executable.description), + arguments: \(self.arguments.description), + environment: \(self.environment.description), + workingDirectory: \(self.workingDirectory), + platformOptions: \(self.platformOptions.description(withIndent: 1)) + ) + """ + } + + public var debugDescription: String { + return """ + Configuration( + executable: \(self.executable.debugDescription), + arguments: \(self.arguments.debugDescription), + environment: \(self.environment.debugDescription), + workingDirectory: \(self.workingDirectory), + platformOptions: \(self.platformOptions.description(withIndent: 1)) + ) + """ + } +} + +// MARK: - Cleanup +extension Configuration { + /// Close each input individually, and throw the first error if there's multiple errors thrown + @Sendable + @available(macOS 15.0, *) // FIXME: manually added availability + private func cleanup< + Output: OutputProtocol, + Error: OutputProtocol + >( + execution: Execution, + inputPipe: CreatedPipe, + outputPipe: CreatedPipe, + errorPipe: CreatedPipe, + childSide: Bool, + parentSide: Bool, + attemptToTerminateSubProcess: Bool + ) async throws { + func captureError(_ work: () throws -> Void) -> Swift.Error? { + do { + try work() + return nil + } catch { + // Ignore badFileDescriptor for double close + return error + } + } + + guard childSide || parentSide || attemptToTerminateSubProcess else { + return + } + + // Attempt to teardown the subprocess + if attemptToTerminateSubProcess { + await execution.teardown( + using: self.platformOptions.teardownSequence + ) + } + + var inputError: Swift.Error? + var outputError: Swift.Error? + var errorError: Swift.Error? // lol + + if childSide { + inputError = captureError { + try inputPipe.readFileDescriptor?.safelyClose() + } + outputError = captureError { + try outputPipe.writeFileDescriptor?.safelyClose() + } + errorError = captureError { + try errorPipe.writeFileDescriptor?.safelyClose() + } + } + + if parentSide { + inputError = captureError { + try inputPipe.writeFileDescriptor?.safelyClose() + } + outputError = captureError { + try outputPipe.readFileDescriptor?.safelyClose() + } + errorError = captureError { + try errorPipe.readFileDescriptor?.safelyClose() + } + } + + if let inputError = inputError { + throw inputError + } + + if let outputError = outputError { + throw outputError + } + + if let errorError = errorError { + throw errorError + } + } + + /// Close each input individually, and throw the first error if there's multiple errors thrown + @Sendable + internal func cleanupPreSpawn( + input: CreatedPipe, + output: CreatedPipe, + error: CreatedPipe + ) throws { + var inputError: Swift.Error? + var outputError: Swift.Error? + var errorError: Swift.Error? + + do { + try input.readFileDescriptor?.safelyClose() + try input.writeFileDescriptor?.safelyClose() + } catch { + inputError = error + } + + do { + try output.readFileDescriptor?.safelyClose() + try output.writeFileDescriptor?.safelyClose() + } catch { + outputError = error + } + + do { + try error.readFileDescriptor?.safelyClose() + try error.writeFileDescriptor?.safelyClose() + } catch { + errorError = error + } + + if let inputError = inputError { + throw inputError + } + if let outputError = outputError { + throw outputError + } + if let errorError = errorError { + throw errorError + } + } +} + +// MARK: - Executable + +/// `Executable` defines how the executable should +/// be looked up for execution. +public struct Executable: Sendable, Hashable { + internal enum Storage: Sendable, Hashable { + case executable(String) + case path(FilePath) + } + + internal let storage: Storage + + private init(_config: Storage) { + self.storage = _config + } + + /// Locate the executable by its name. + /// `Subprocess` will use `PATH` value to + /// determine the full path to the executable. + public static func name(_ executableName: String) -> Self { + return .init(_config: .executable(executableName)) + } + /// Locate the executable by its full path. + /// `Subprocess` will use this path directly. + public static func path(_ filePath: FilePath) -> Self { + return .init(_config: .path(filePath)) + } + /// Returns the full executable path given the environment value. + public func resolveExecutablePath(in environment: Environment) throws -> FilePath { + let path = try self.resolveExecutablePath(withPathValue: environment.pathValue()) + return FilePath(path) + } +} + +extension Executable: CustomStringConvertible, CustomDebugStringConvertible { + public var description: String { + switch storage { + case .executable(let executableName): + return executableName + case .path(let filePath): + return filePath.string + } + } + + public var debugDescription: String { + switch storage { + case .executable(let string): + return "executable(\(string))" + case .path(let filePath): + return "path(\(filePath.string))" + } + } +} + +extension Executable { + internal func possibleExecutablePaths( + withPathValue pathValue: String? + ) -> Set { + switch self.storage { + case .executable(let executableName): + #if os(Windows) + // Windows CreateProcessW accepts executable name directly + return Set([executableName]) + #else + var results: Set = [] + // executableName could be a full path + results.insert(executableName) + // Get $PATH from environment + let searchPaths: Set + if let pathValue = pathValue { + let localSearchPaths = pathValue.split(separator: ":").map { String($0) } + searchPaths = Set(localSearchPaths).union(Self.defaultSearchPaths) + } else { + searchPaths = Self.defaultSearchPaths + } + for path in searchPaths { + results.insert( + FilePath(path).appending(executableName).string + ) + } + return results + #endif + case .path(let executablePath): + return Set([executablePath.string]) + } + } +} + +// MARK: - Arguments + +/// A collection of arguments to pass to the subprocess. +public struct Arguments: Sendable, ExpressibleByArrayLiteral, Hashable { + public typealias ArrayLiteralElement = String + + internal let storage: [StringOrRawBytes] + internal let executablePathOverride: StringOrRawBytes? + + /// Create an Arguments object using the given literal values + public init(arrayLiteral elements: String...) { + self.storage = elements.map { .string($0) } + self.executablePathOverride = nil + } + /// Create an Arguments object using the given array + public init(_ array: [String]) { + self.storage = array.map { .string($0) } + self.executablePathOverride = nil + } + + #if !os(Windows) // Windows does NOT support arg0 override + /// Create an `Argument` object using the given values, but + /// override the first Argument value to `executablePathOverride`. + /// If `executablePathOverride` is nil, + /// `Arguments` will automatically use the executable path + /// as the first argument. + /// - Parameters: + /// - executablePathOverride: the value to override the first argument. + /// - remainingValues: the rest of the argument value + public init(executablePathOverride: String?, remainingValues: [String]) { + self.storage = remainingValues.map { .string($0) } + if let executablePathOverride = executablePathOverride { + self.executablePathOverride = .string(executablePathOverride) + } else { + self.executablePathOverride = nil + } + } + + /// Create an `Argument` object using the given values, but + /// override the first Argument value to `executablePathOverride`. + /// If `executablePathOverride` is nil, + /// `Arguments` will automatically use the executable path + /// as the first argument. + /// - Parameters: + /// - executablePathOverride: the value to override the first argument. + /// - remainingValues: the rest of the argument value + public init(executablePathOverride: [UInt8]?, remainingValues: [[UInt8]]) { + self.storage = remainingValues.map { .rawBytes($0) } + if let override = executablePathOverride { + self.executablePathOverride = .rawBytes(override) + } else { + self.executablePathOverride = nil + } + } + + public init(_ array: [[UInt8]]) { + self.storage = array.map { .rawBytes($0) } + self.executablePathOverride = nil + } + #endif +} + +extension Arguments: CustomStringConvertible, CustomDebugStringConvertible { + public var description: String { + var result: [String] = self.storage.map(\.description) + + if let override = self.executablePathOverride { + result.insert("override\(override.description)", at: 0) + } + return result.description + } + + public var debugDescription: String { return self.description } +} + +// MARK: - Environment + +/// A set of environment variables to use when executing the subprocess. +public struct Environment: Sendable, Hashable { + internal enum Configuration: Sendable, Hashable { + case inherit([String: String]) + case custom([String: String]) + #if !os(Windows) + case rawBytes([[UInt8]]) + #endif + } + + internal let config: Configuration + + init(config: Configuration) { + self.config = config + } + /// Child process should inherit the same environment + /// values from its parent process. + public static var inherit: Self { + return .init(config: .inherit([:])) + } + /// Override the provided `newValue` in the existing `Environment` + public func updating(_ newValue: [String: String]) -> Self { + return .init(config: .inherit(newValue)) + } + /// Use custom environment variables + public static func custom(_ newValue: [String: String]) -> Self { + return .init(config: .custom(newValue)) + } + + #if !os(Windows) + /// Use custom environment variables of raw bytes + public static func custom(_ newValue: [[UInt8]]) -> Self { + return .init(config: .rawBytes(newValue)) + } + #endif +} + +extension Environment: CustomStringConvertible, CustomDebugStringConvertible { + public var description: String { + switch self.config { + case .custom(let customDictionary): + return """ + Custom environment: + \(customDictionary) + """ + case .inherit(let updateValue): + return """ + Inherting current environment with updates: + \(updateValue) + """ + #if !os(Windows) + case .rawBytes(let rawBytes): + return """ + Raw bytes: + \(rawBytes) + """ + #endif + } + } + + public var debugDescription: String { + return self.description + } + + internal static func currentEnvironmentValues() -> [String: String] { + return self.withCopiedEnv { environments in + var results: [String: String] = [:] + for env in environments { + let environmentString = String(cString: env) + + #if os(Windows) + // Windows GetEnvironmentStringsW API can return + // magic environment variables set by the cmd shell + // that starts with `=` + // We should exclude these values + if environmentString.utf8.first == Character("=").utf8.first { + continue + } + #endif // os(Windows) + + guard let delimiter = environmentString.firstIndex(of: "=") else { + continue + } + + let key = String(environmentString[environmentString.startIndex.. UnsafeMutablePointer { + switch self { + case .string(let string): + return strdup(string) + case .rawBytes(let rawBytes): + return strdup(rawBytes) + } + } + + var stringValue: String? { + switch self { + case .string(let string): + return string + case .rawBytes(let rawBytes): + return String(decoding: rawBytes, as: UTF8.self) + } + } + + var description: String { + switch self { + case .string(let string): + return string + case .rawBytes(let bytes): + return bytes.description + } + } + + var count: Int { + switch self { + case .string(let string): + return string.count + case .rawBytes(let rawBytes): + return strnlen(rawBytes, Int.max) + } + } + + func hash(into hasher: inout Hasher) { + // If Raw bytes is valid UTF8, hash it as so + switch self { + case .string(let string): + hasher.combine(string) + case .rawBytes(let bytes): + if let stringValue = self.stringValue { + hasher.combine(stringValue) + } else { + hasher.combine(bytes) + } + } + } +} + +/// A simple wrapper on `FileDescriptor` plus a flag indicating +/// whether it should be closed automactially when done. +internal struct TrackedFileDescriptor: Hashable { + internal let closeWhenDone: Bool + internal let wrapped: FileDescriptor + + internal init( + _ wrapped: FileDescriptor, + closeWhenDone: Bool + ) { + self.wrapped = wrapped + self.closeWhenDone = closeWhenDone + } + + internal func safelyClose() throws { + guard self.closeWhenDone else { + return + } + + do { + try self.wrapped.close() + } catch { + guard let errno: Errno = error as? Errno else { + throw error + } + if errno != .badFileDescriptor { + throw errno + } + } + } + + internal var platformDescriptor: PlatformFileDescriptor { + return self.wrapped.platformDescriptor + } +} + +internal struct CreatedPipe { + internal let readFileDescriptor: TrackedFileDescriptor? + internal let writeFileDescriptor: TrackedFileDescriptor? + + internal init( + readFileDescriptor: TrackedFileDescriptor?, + writeFileDescriptor: TrackedFileDescriptor? + ) { + self.readFileDescriptor = readFileDescriptor + self.writeFileDescriptor = writeFileDescriptor + } + + internal init(closeWhenDone: Bool) throws { + let pipe = try FileDescriptor.pipe() + + self.readFileDescriptor = .init( + pipe.readEnd, + closeWhenDone: closeWhenDone + ) + self.writeFileDescriptor = .init( + pipe.writeEnd, + closeWhenDone: closeWhenDone + ) + } +} + +extension FilePath { + static var currentWorkingDirectory: Self { + let path = getcwd(nil, 0)! + defer { free(path) } + return .init(String(cString: path)) + } +} + +extension Optional where Wrapped: Collection { + func withOptionalUnsafeBufferPointer( + _ body: ((UnsafeBufferPointer)?) throws -> Result + ) rethrows -> Result { + switch self { + case .some(let wrapped): + guard let array: [Wrapped.Element] = wrapped as? Array else { + return try body(nil) + } + return try array.withUnsafeBufferPointer { ptr in + return try body(ptr) + } + case .none: + return try body(nil) + } + } +} + +extension Optional where Wrapped == String { + func withOptionalCString( + _ body: ((UnsafePointer)?) throws -> Result + ) rethrows -> Result { + switch self { + case .none: + return try body(nil) + case .some(let wrapped): + return try wrapped.withCString { + return try body($0) + } + } + } + + var stringValue: String { + return self ?? "nil" + } +} + +internal func withAsyncTaskCleanupHandler( + _ body: () async throws -> Result, + onCleanup handler: @Sendable @escaping () async -> Void, + isolation: isolated (any Actor)? = #isolation +) async rethrows -> Result { + return try await withThrowingTaskGroup( + of: Void.self, + returning: Result.self + ) { group in + group.addTask { + // Keep this task sleep indefinitely until the parent task is cancelled. + // `Task.sleep` throws `CancellationError` when the task is canceled + // before the time ends. We then run the cancel handler. + do { while true { try await Task.sleep(nanoseconds: 1_000_000_000) } } catch {} + // Run task cancel handler + await handler() + } + + do { + let result = try await body() + group.cancelAll() + return result + } catch { + await handler() + throw error + } + } +} diff --git a/Sources/_Subprocess/Error.swift b/Sources/_Subprocess/Error.swift new file mode 100644 index 000000000..bf1c91147 --- /dev/null +++ b/Sources/_Subprocess/Error.swift @@ -0,0 +1,141 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +#if canImport(Darwin) +import Darwin +#elseif canImport(Bionic) +import Bionic +#elseif canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#elseif canImport(WinSDK) +import WinSDK +#endif + +/// Error thrown from Subprocess +public struct SubprocessError: Swift.Error, Hashable, Sendable { + /// The error code of this error + public let code: SubprocessError.Code + /// The underlying error that caused this error, if any + public let underlyingError: UnderlyingError? +} + +// MARK: - Error Codes +extension SubprocessError { + /// A SubprocessError Code + public struct Code: Hashable, Sendable { + internal enum Storage: Hashable, Sendable { + case spawnFailed + case executableNotFound(String) + case failedToChangeWorkingDirectory(String) + case failedToReadFromSubprocess + case failedToWriteToSubprocess + case failedToMonitorProcess + // Signal + case failedToSendSignal(Int32) + // Windows Only + case failedToTerminate + case failedToSuspend + case failedToResume + case failedToCreatePipe + case invalidWindowsPath(String) + } + + public var value: Int { + switch self.storage { + case .spawnFailed: + return 0 + case .executableNotFound(_): + return 1 + case .failedToChangeWorkingDirectory(_): + return 2 + case .failedToReadFromSubprocess: + return 3 + case .failedToWriteToSubprocess: + return 4 + case .failedToMonitorProcess: + return 5 + case .failedToSendSignal(_): + return 6 + case .failedToTerminate: + return 7 + case .failedToSuspend: + return 8 + case .failedToResume: + return 9 + case .failedToCreatePipe: + return 10 + case .invalidWindowsPath(_): + return 11 + } + } + + internal let storage: Storage + + internal init(_ storage: Storage) { + self.storage = storage + } + } +} + +// MARK: - Description +extension SubprocessError: CustomStringConvertible, CustomDebugStringConvertible { + public var description: String { + switch self.code.storage { + case .spawnFailed: + return "Failed to spawn the new process." + case .executableNotFound(let executableName): + return "Executable \"\(executableName)\" is not found or cannot be executed." + case .failedToChangeWorkingDirectory(let workingDirectory): + return "Failed to set working directory to \"\(workingDirectory)\"." + case .failedToReadFromSubprocess: + return "Failed to read bytes from the child process with underlying error: \(self.underlyingError!)" + case .failedToWriteToSubprocess: + return "Failed to write bytes to the child process." + case .failedToMonitorProcess: + return "Failed to monitor the state of child process with underlying error: \(self.underlyingError!)" + case .failedToSendSignal(let signal): + return "Failed to send signal \(signal) to the child process." + case .failedToTerminate: + return "Failed to terminate the child process." + case .failedToSuspend: + return "Failed to suspend the child process." + case .failedToResume: + return "Failed to resume the child process." + case .failedToCreatePipe: + return "Failed to create a pipe to communicate to child process." + case .invalidWindowsPath(let badPath): + return "\"\(badPath)\" is not a valid Windows path." + } + } + + public var debugDescription: String { self.description } +} + +extension SubprocessError { + /// The underlying error that caused this SubprocessError. + /// - On Unix-like systems, `UnderlyingError` wraps `errno` from libc; + /// - On Windows, `UnderlyingError` wraps Windows Error code + public struct UnderlyingError: Swift.Error, RawRepresentable, Hashable, Sendable { + #if os(Windows) + public typealias RawValue = DWORD + #else + public typealias RawValue = Int32 + #endif + + public let rawValue: RawValue + + public init(rawValue: RawValue) { + self.rawValue = rawValue + } + } +} diff --git a/Sources/_Subprocess/Execution.swift b/Sources/_Subprocess/Execution.swift new file mode 100644 index 000000000..8da9b4924 --- /dev/null +++ b/Sources/_Subprocess/Execution.swift @@ -0,0 +1,192 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +#if canImport(System) +import System +#else +@preconcurrency import SystemPackage +#endif + +#if canImport(Darwin) +import Darwin +#elseif canImport(Bionic) +import Bionic +#elseif canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#elseif canImport(WinSDK) +import WinSDK +#endif + +import Synchronization + +/// An object that repersents a subprocess that has been +/// executed. You can use this object to send signals to the +/// child process as well as stream its output and error. +@available(macOS 15.0, *) // FIXME: manually added availability +public final class Execution< + Output: OutputProtocol, + Error: OutputProtocol +>: Sendable { + /// The process identifier of the current execution + public let processIdentifier: ProcessIdentifier + + internal let output: Output + internal let error: Error + internal let outputPipe: CreatedPipe + internal let errorPipe: CreatedPipe + internal let outputConsumptionState: Atomic + #if os(Windows) + internal let consoleBehavior: PlatformOptions.ConsoleBehavior + + init( + processIdentifier: ProcessIdentifier, + output: Output, + error: Error, + outputPipe: CreatedPipe, + errorPipe: CreatedPipe, + consoleBehavior: PlatformOptions.ConsoleBehavior + ) { + self.processIdentifier = processIdentifier + self.output = output + self.error = error + self.outputPipe = outputPipe + self.errorPipe = errorPipe + self.outputConsumptionState = Atomic(0) + self.consoleBehavior = consoleBehavior + } + #else + init( + processIdentifier: ProcessIdentifier, + output: Output, + error: Error, + outputPipe: CreatedPipe, + errorPipe: CreatedPipe + ) { + self.processIdentifier = processIdentifier + self.output = output + self.error = error + self.outputPipe = outputPipe + self.errorPipe = errorPipe + self.outputConsumptionState = Atomic(0) + } + #endif // os(Windows) +} + +@available(macOS 15.0, *) // FIXME: manually added availability +extension Execution where Output == SequenceOutput { + /// The standard output of the subprocess. + /// + /// Accessing this property will **fatalError** if this property was + /// accessed multiple times. Subprocess communicates with parent process + /// via pipe under the hood and each pipe can only be consumed once. + @available(macOS 15.0, *) // FIXME: manually added availability + public var standardOutput: some AsyncSequence { + let consumptionState = self.outputConsumptionState.bitwiseXor( + OutputConsumptionState.standardOutputConsumed.rawValue, + ordering: .relaxed + ).newValue + + guard OutputConsumptionState(rawValue: consumptionState).contains(.standardOutputConsumed), + let fd = self.outputPipe.readFileDescriptor + else { + fatalError("The standard output has already been consumed") + } + return AsyncBufferSequence(fileDescriptor: fd) + } +} + +@available(macOS 15.0, *) // FIXME: manually added availability +extension Execution where Error == SequenceOutput { + /// The standard error of the subprocess. + /// + /// Accessing this property will **fatalError** if this property was + /// accessed multiple times. Subprocess communicates with parent process + /// via pipe under the hood and each pipe can only be consumed once. + @available(macOS 15.0, *) // FIXME: manually added availability + public var standardError: some AsyncSequence { + let consumptionState = self.outputConsumptionState.bitwiseXor( + OutputConsumptionState.standardErrorConsumed.rawValue, + ordering: .relaxed + ).newValue + + guard OutputConsumptionState(rawValue: consumptionState).contains(.standardErrorConsumed), + let fd = self.errorPipe.readFileDescriptor + else { + fatalError("The standard output has already been consumed") + } + return AsyncBufferSequence(fileDescriptor: fd) + } +} + +// MARK: - Output Capture +internal enum OutputCapturingState: Sendable { + case standardOutputCaptured(Output) + case standardErrorCaptured(Error) +} + +internal struct OutputConsumptionState: OptionSet { + typealias RawValue = UInt8 + + internal let rawValue: UInt8 + + internal init(rawValue: UInt8) { + self.rawValue = rawValue + } + + static let standardOutputConsumed: Self = .init(rawValue: 0b0001) + static let standardErrorConsumed: Self = .init(rawValue: 0b0010) +} + +internal typealias CapturedIOs< + Output: Sendable, + Error: Sendable +> = (standardOutput: Output, standardError: Error) + +@available(macOS 15.0, *) // FIXME: manually added availability +extension Execution { + internal func captureIOs() async throws -> CapturedIOs< + Output.OutputType, Error.OutputType + > { + return try await withThrowingTaskGroup( + of: OutputCapturingState.self + ) { group in + group.addTask { + let stdout = try await self.output.captureOutput( + from: self.outputPipe.readFileDescriptor + ) + return .standardOutputCaptured(stdout) + } + group.addTask { + let stderr = try await self.error.captureOutput( + from: self.errorPipe.readFileDescriptor + ) + return .standardErrorCaptured(stderr) + } + + var stdout: Output.OutputType! + var stderror: Error.OutputType! + while let state = try await group.next() { + switch state { + case .standardOutputCaptured(let output): + stdout = output + case .standardErrorCaptured(let error): + stderror = error + } + } + return ( + standardOutput: stdout, + standardError: stderror + ) + } + } +} diff --git a/Sources/_Subprocess/IO/Input.swift b/Sources/_Subprocess/IO/Input.swift new file mode 100644 index 000000000..5aad5d94e --- /dev/null +++ b/Sources/_Subprocess/IO/Input.swift @@ -0,0 +1,315 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +#if canImport(System) +import System +#else +@preconcurrency import SystemPackage +#endif + +#if SubprocessFoundation + +#if canImport(Darwin) +// On Darwin always prefer system Foundation +import Foundation +#else +// On other platforms prefer FoundationEssentials +import FoundationEssentials +#endif + +#endif // SubprocessFoundation + +// MARK: - Input + +/// `InputProtocol` defines the `write(with:)` method that a type +/// must implement to serve as the input source for a subprocess. +public protocol InputProtocol: Sendable, ~Copyable { + /// Asynchronously write the input to the subprocess using the + /// write file descriptor + func write(with writer: StandardInputWriter) async throws +} + +/// A concrete `Input` type for subprocesses that indicates +/// the absence of input to the subprocess. On Unix-like systems, +/// `NoInput` redirects the standard input of the subprocess +/// to `/dev/null`, while on Windows, it does not bind any +/// file handle to the subprocess standard input handle. +public struct NoInput: InputProtocol { + internal func createPipe() throws -> CreatedPipe { + #if os(Windows) + // On Windows, instead of binding to dev null, + // we don't set the input handle in the `STARTUPINFOW` + // to signal no input + return CreatedPipe( + readFileDescriptor: nil, + writeFileDescriptor: nil + ) + #else + let devnull: FileDescriptor = try .openDevNull(withAcessMode: .readOnly) + return CreatedPipe( + readFileDescriptor: .init(devnull, closeWhenDone: true), + writeFileDescriptor: nil + ) + #endif + } + + public func write(with writer: StandardInputWriter) async throws { + // noop + } + + internal init() {} +} + +/// A concrete `Input` type for subprocesses that +/// reads input from a specified `FileDescriptor`. +/// Developers have the option to instruct the `Subprocess` to +/// automatically close the provided `FileDescriptor` +/// after the subprocess is spawned. +public struct FileDescriptorInput: InputProtocol { + private let fileDescriptor: FileDescriptor + private let closeAfterSpawningProcess: Bool + + internal func createPipe() throws -> CreatedPipe { + return CreatedPipe( + readFileDescriptor: .init( + self.fileDescriptor, + closeWhenDone: self.closeAfterSpawningProcess + ), + writeFileDescriptor: nil + ) + } + + public func write(with writer: StandardInputWriter) async throws { + // noop + } + + internal init( + fileDescriptor: FileDescriptor, + closeAfterSpawningProcess: Bool + ) { + self.fileDescriptor = fileDescriptor + self.closeAfterSpawningProcess = closeAfterSpawningProcess + } +} + +/// A concrete `Input` type for subprocesses that reads input +/// from a given type conforming to `StringProtocol`. +/// Developers can specify the string encoding to use when +/// encoding the string to data, which defaults to UTF-8. +public struct StringInput< + InputString: StringProtocol & Sendable, + Encoding: Unicode.Encoding +>: InputProtocol { + private let string: InputString + + public func write(with writer: StandardInputWriter) async throws { + guard let array = self.string.byteArray(using: Encoding.self) else { + return + } + _ = try await writer.write(array) + } + + internal init(string: InputString, encoding: Encoding.Type) { + self.string = string + } +} + +/// A concrete `Input` type for subprocesses that reads input +/// from a given `UInt8` Array. +public struct ArrayInput: InputProtocol { + private let array: [UInt8] + + public func write(with writer: StandardInputWriter) async throws { + _ = try await writer.write(self.array) + } + + internal init(array: [UInt8]) { + self.array = array + } +} + +/// A concrete `Input` type for subprocess that indicates that +/// the Subprocess should read its input from `StandardInputWriter`. +public struct CustomWriteInput: InputProtocol { + public func write(with writer: StandardInputWriter) async throws { + // noop + } + + internal init() {} +} + +extension InputProtocol where Self == NoInput { + /// Create a Subprocess input that specfies there is no input + public static var none: Self { .init() } +} + +extension InputProtocol where Self == FileDescriptorInput { + /// Create a Subprocess input from a `FileDescriptor` and + /// specify whether the `FileDescriptor` should be closed + /// after the process is spawned. + public static func fileDescriptor( + _ fd: FileDescriptor, + closeAfterSpawningProcess: Bool + ) -> Self { + return .init( + fileDescriptor: fd, + closeAfterSpawningProcess: closeAfterSpawningProcess + ) + } +} + +extension InputProtocol { + /// Create a Subprocess input from a `Array` of `UInt8`. + public static func array( + _ array: [UInt8] + ) -> Self where Self == ArrayInput { + return ArrayInput(array: array) + } + + /// Create a Subprocess input from a type that conforms to `StringProtocol` + public static func string< + InputString: StringProtocol & Sendable + >( + _ string: InputString + ) -> Self where Self == StringInput { + return .init(string: string, encoding: UTF8.self) + } + + /// Create a Subprocess input from a type that conforms to `StringProtocol` + public static func string< + InputString: StringProtocol & Sendable, + Encoding: Unicode.Encoding + >( + _ string: InputString, + using encoding: Encoding.Type + ) -> Self where Self == StringInput { + return .init(string: string, encoding: encoding) + } +} + +extension InputProtocol { + internal func createPipe() throws -> CreatedPipe { + if let noInput = self as? NoInput { + return try noInput.createPipe() + } else if let fdInput = self as? FileDescriptorInput { + return try fdInput.createPipe() + } + // Base implementation + return try CreatedPipe(closeWhenDone: true) + } +} + +// MARK: - StandardInputWriter + +/// A writer that writes to the standard input of the subprocess. +public final actor StandardInputWriter: Sendable { + + internal let fileDescriptor: TrackedFileDescriptor + + init(fileDescriptor: TrackedFileDescriptor) { + self.fileDescriptor = fileDescriptor + } + + /// Write an array of UInt8 to the standard input of the subprocess. + /// - Parameter array: The sequence of bytes to write. + /// - Returns number of bytes written. + public func write( + _ array: [UInt8] + ) async throws -> Int { + return try await self.fileDescriptor.wrapped.write(array) + } + + /// Write a StringProtocol to the standard input of the subprocess. + /// - Parameters: + /// - string: The string to write. + /// - encoding: The encoding to use when converting string to bytes + /// - Returns number of bytes written. + public func write( + _ string: some StringProtocol, + using encoding: Encoding.Type = UTF8.self + ) async throws -> Int { + if let array = string.byteArray(using: encoding) { + return try await self.write(array) + } + return 0 + } + + /// Signal all writes are finished + public func finish() async throws { + try self.fileDescriptor.safelyClose() + } +} + +extension StringProtocol { + #if SubprocessFoundation + private func convertEncoding( + _ encoding: Encoding.Type + ) -> String.Encoding? { + switch encoding { + case is UTF8.Type: + return .utf8 + case is UTF16.Type: + return .utf16 + case is UTF32.Type: + return .utf32 + default: + return nil + } + } + #endif + package func byteArray(using encoding: Encoding.Type) -> [UInt8]? { + if Encoding.self == Unicode.ASCII.self { + let isASCII = self.utf8.allSatisfy { + return Character(Unicode.Scalar($0)).isASCII + } + + guard isASCII else { + return nil + } + return Array(self.utf8) + } + if Encoding.self == UTF8.self { + return Array(self.utf8) + } + if Encoding.self == UTF16.self { + return Array(self.utf16).flatMap { input in + var uint16: UInt16 = input + return withUnsafeBytes(of: &uint16) { ptr in + Array(ptr) + } + } + } + #if SubprocessFoundation + if let stringEncoding = self.convertEncoding(encoding), + let encoded = self.data(using: stringEncoding) + { + return Array(encoded) + } + return nil + #else + return nil + #endif + } +} + +extension String { + package init( + decodingBytes bytes: [T], + as encoding: Encoding.Type + ) { + self = bytes.withUnsafeBytes { raw in + String( + decoding: raw.bindMemory(to: Encoding.CodeUnit.self).lazy.map { $0 }, + as: encoding + ) + } + } +} diff --git a/Sources/_Subprocess/IO/Output.swift b/Sources/_Subprocess/IO/Output.swift new file mode 100644 index 000000000..be186dd6b --- /dev/null +++ b/Sources/_Subprocess/IO/Output.swift @@ -0,0 +1,298 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +#if canImport(System) +import System +#else +@preconcurrency import SystemPackage +#endif +internal import Dispatch + +// MARK: - Output + +/// `OutputProtocol` specifies the set of methods that a type +/// must implement to serve as the output target for a subprocess. +/// Instead of developing custom implementations of `OutputProtocol`, +/// it is recommended to utilize the default implementations provided +/// by the `Subprocess` library to specify the output handling requirements. +public protocol OutputProtocol: Sendable, ~Copyable { + associatedtype OutputType: Sendable + + /// Convert the output from buffer to expected output type + func output(from buffer: some Sequence) throws -> OutputType + + /// The max amount of data to collect for this output. + var maxSize: Int { get } +} + +extension OutputProtocol { + /// The max amount of data to collect for this output. + public var maxSize: Int { 128 * 1024 } +} + +/// A concrete `Output` type for subprocesses that indicates that +/// the `Subprocess` should not collect or redirect output +/// from the child process. On Unix-like systems, `DiscardedOutput` +/// redirects the standard output of the subprocess to `/dev/null`, +/// while on Windows, it does not bind any file handle to the +/// subprocess standard output handle. +public struct DiscardedOutput: OutputProtocol { + public typealias OutputType = Void + + internal func createPipe() throws -> CreatedPipe { + #if os(Windows) + // On Windows, instead of binding to dev null, + // we don't set the input handle in the `STARTUPINFOW` + // to signal no output + return CreatedPipe( + readFileDescriptor: nil, + writeFileDescriptor: nil + ) + #else + let devnull: FileDescriptor = try .openDevNull(withAcessMode: .readOnly) + return CreatedPipe( + readFileDescriptor: .init(devnull, closeWhenDone: true), + writeFileDescriptor: nil + ) + #endif + } + + internal init() {} +} + +/// A concrete `Output` type for subprocesses that +/// writes output to a specified `FileDescriptor`. +/// Developers have the option to instruct the `Subprocess` to +/// automatically close the provided `FileDescriptor` +/// after the subprocess is spawned. +public struct FileDescriptorOutput: OutputProtocol { + public typealias OutputType = Void + + private let closeAfterSpawningProcess: Bool + private let fileDescriptor: FileDescriptor + + internal func createPipe() throws -> CreatedPipe { + return CreatedPipe( + readFileDescriptor: nil, + writeFileDescriptor: .init( + self.fileDescriptor, + closeWhenDone: self.closeAfterSpawningProcess + ) + ) + } + + internal init( + fileDescriptor: FileDescriptor, + closeAfterSpawningProcess: Bool + ) { + self.fileDescriptor = fileDescriptor + self.closeAfterSpawningProcess = closeAfterSpawningProcess + } +} + +/// A concrete `Output` type for subprocesses that collects output +/// from the subprocess as `String` with the given encoding. +/// This option must be used with he `run()` method that +/// returns a `CollectedResult`. +public struct StringOutput: OutputProtocol { + public typealias OutputType = String? + public let maxSize: Int + private let encoding: Encoding.Type + + public func output(from buffer: some Sequence) throws -> String? { + // FIXME: Span to String + let array = Array(buffer) + return String(decodingBytes: array, as: Encoding.self) + } + + internal init(limit: Int, encoding: Encoding.Type) { + self.maxSize = limit + self.encoding = encoding + } +} + +/// A concrete `Output` type for subprocesses that collects output +/// from the subprocess as `[UInt8]`. This option must be used with +/// the `run()` method that returns a `CollectedResult` +public struct BytesOutput: OutputProtocol { + public typealias OutputType = [UInt8] + public let maxSize: Int + + internal func captureOutput(from fileDescriptor: TrackedFileDescriptor?) async throws -> [UInt8] { + return try await withCheckedThrowingContinuation { continuation in + guard let fileDescriptor = fileDescriptor else { + // Show not happen due to type system constraints + fatalError("Trying to capture output without file descriptor") + } + fileDescriptor.wrapped.readUntilEOF(upToLength: self.maxSize) { result in + switch result { + case .success(let data): + // FIXME: remove workaround for + // rdar://143992296 + // https://github.com/swiftlang/swift-subprocess/issues/3 + #if os(Windows) + continuation.resume(returning: data) + #else + continuation.resume(returning: data.array()) + #endif + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } + + public func output(from buffer: some Sequence) throws -> [UInt8] { + fatalError("Not implemented") + } + + internal init(limit: Int) { + self.maxSize = limit + } +} + +/// A concrete `Output` type for subprocesses that redirects +/// the child output to the `.standardOutput` (a sequence) or `.standardError` +/// property of `Execution`. This output type is +/// only applicable to the `run()` family that takes a custom closure. +public struct SequenceOutput: OutputProtocol { + public typealias OutputType = Void + + internal init() {} +} + +extension OutputProtocol where Self == DiscardedOutput { + /// Create a Subprocess output that discards the output + public static var discarded: Self { .init() } +} + +extension OutputProtocol where Self == FileDescriptorOutput { + /// Create a Subprocess output that writes output to a `FileDescriptor` + /// and optionally close the `FileDescriptor` once process spawned. + public static func fileDescriptor( + _ fd: FileDescriptor, + closeAfterSpawningProcess: Bool + ) -> Self { + return .init(fileDescriptor: fd, closeAfterSpawningProcess: closeAfterSpawningProcess) + } +} + +extension OutputProtocol where Self == StringOutput { + /// Create a `Subprocess` output that collects output as + /// UTF8 String with 128kb limit. + public static var string: Self { + .init(limit: 128 * 1024, encoding: UTF8.self) + } +} + +extension OutputProtocol { + /// Create a `Subprocess` output that collects output as + /// `String` using the given encoding up to limit it bytes. + public static func string( + limit: Int, + encoding: Encoding.Type + ) -> Self where Self == StringOutput { + return .init(limit: limit, encoding: encoding) + } +} + +extension OutputProtocol where Self == BytesOutput { + /// Create a `Subprocess` output that collects output as + /// `Buffer` with 128kb limit. + public static var bytes: Self { .init(limit: 128 * 1024) } + + /// Create a `Subprocess` output that collects output as + /// `Buffer` up to limit it bytes. + public static func bytes(limit: Int) -> Self { + return .init(limit: limit) + } +} + +extension OutputProtocol where Self == SequenceOutput { + /// Create a `Subprocess` output that redirects the output + /// to the `.standardOutput` (or `.standardError`) property + /// of `Execution` as `AsyncSequence`. + public static var sequence: Self { .init() } +} + +// MARK: - Default Implementations +extension OutputProtocol { + @_disfavoredOverload + internal func createPipe() throws -> CreatedPipe { + if let discard = self as? DiscardedOutput { + return try discard.createPipe() + } else if let fdOutput = self as? FileDescriptorOutput { + return try fdOutput.createPipe() + } + // Base pipe based implementation for everything else + return try CreatedPipe(closeWhenDone: true) + } + + /// Capture the output from the subprocess up to maxSize + @_disfavoredOverload + internal func captureOutput( + from fileDescriptor: TrackedFileDescriptor? + ) async throws -> OutputType { + if let bytesOutput = self as? BytesOutput { + return try await bytesOutput.captureOutput(from: fileDescriptor) as! Self.OutputType + } + return try await withCheckedThrowingContinuation { continuation in + if OutputType.self == Void.self { + continuation.resume(returning: () as! OutputType) + return + } + guard let fileDescriptor = fileDescriptor else { + // Show not happen due to type system constraints + fatalError("Trying to capture output without file descriptor") + } + + fileDescriptor.wrapped.readUntilEOF(upToLength: self.maxSize) { result in + do { + switch result { + case .success(let data): + // FIXME: remove workaround for + // rdar://143992296 + // https://github.com/swiftlang/swift-subprocess/issues/3 + let output = try self.output(from: data) + continuation.resume(returning: output) + case .failure(let error): + continuation.resume(throwing: error) + } + } catch { + continuation.resume(throwing: error) + } + } + } + } +} + +extension OutputProtocol where OutputType == Void { + internal func captureOutput(from fileDescriptor: TrackedFileDescriptor?) async throws {} + + public func output(from buffer: some Sequence) throws { + // noop + } +} + +extension DispatchData { + internal func array() -> [UInt8] { + var result: [UInt8]? + self.enumerateBytes { buffer, byteIndex, stop in + let currentChunk = Array(UnsafeRawBufferPointer(buffer)) + if result == nil { + result = currentChunk + } else { + result?.append(contentsOf: currentChunk) + } + } + return result ?? [] + } +} diff --git a/Sources/_Subprocess/LockedState.swift b/Sources/_Subprocess/LockedState.swift deleted file mode 100644 index e095668ca..000000000 --- a/Sources/_Subprocess/LockedState.swift +++ /dev/null @@ -1,160 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift.org project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift.org project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -#if canImport(os) -internal import os -#if FOUNDATION_FRAMEWORK && canImport(C.os.lock) -internal import C.os.lock -#endif -#elseif canImport(Bionic) -import Bionic -#elseif canImport(Glibc) -import Glibc -#elseif canImport(Musl) -import Musl -#elseif canImport(WinSDK) -import WinSDK -#endif - -package struct LockedState { - - // Internal implementation for a cheap lock to aid sharing code across platforms - private struct _Lock { -#if canImport(os) - typealias Primitive = os_unfair_lock -#elseif canImport(Bionic) || canImport(Glibc) || canImport(Musl) - typealias Primitive = pthread_mutex_t -#elseif canImport(WinSDK) - typealias Primitive = SRWLOCK -#elseif os(WASI) - // WASI is single-threaded, so we don't need a lock. - typealias Primitive = Void -#endif - - typealias PlatformLock = UnsafeMutablePointer - var _platformLock: PlatformLock - - fileprivate static func initialize(_ platformLock: PlatformLock) { -#if canImport(os) - platformLock.initialize(to: os_unfair_lock()) -#elseif canImport(Bionic) || canImport(Glibc) - pthread_mutex_init(platformLock, nil) -#elseif canImport(WinSDK) - InitializeSRWLock(platformLock) -#elseif os(WASI) - // no-op -#endif - } - - fileprivate static func deinitialize(_ platformLock: PlatformLock) { -#if canImport(Bionic) || canImport(Glibc) - pthread_mutex_destroy(platformLock) -#endif - platformLock.deinitialize(count: 1) - } - - static fileprivate func lock(_ platformLock: PlatformLock) { -#if canImport(os) - os_unfair_lock_lock(platformLock) -#elseif canImport(Bionic) || canImport(Glibc) - pthread_mutex_lock(platformLock) -#elseif canImport(WinSDK) - AcquireSRWLockExclusive(platformLock) -#elseif os(WASI) - // no-op -#endif - } - - static fileprivate func unlock(_ platformLock: PlatformLock) { -#if canImport(os) - os_unfair_lock_unlock(platformLock) -#elseif canImport(Bionic) || canImport(Glibc) - pthread_mutex_unlock(platformLock) -#elseif canImport(WinSDK) - ReleaseSRWLockExclusive(platformLock) -#elseif os(WASI) - // no-op -#endif - } - } - - private class _Buffer: ManagedBuffer { - deinit { - withUnsafeMutablePointerToElements { - _Lock.deinitialize($0) - } - } - } - - private let _buffer: ManagedBuffer - - package init(initialState: State) { - _buffer = _Buffer.create(minimumCapacity: 1, makingHeaderWith: { buf in - buf.withUnsafeMutablePointerToElements { - _Lock.initialize($0) - } - return initialState - }) - } - - package func withLock(_ body: @Sendable (inout State) throws -> T) rethrows -> T { - try withLockUnchecked(body) - } - - package func withLockUnchecked(_ body: (inout State) throws -> T) rethrows -> T { - try _buffer.withUnsafeMutablePointers { state, lock in - _Lock.lock(lock) - defer { _Lock.unlock(lock) } - return try body(&state.pointee) - } - } - - // Ensures the managed state outlives the locked scope. - package func withLockExtendingLifetimeOfState(_ body: @Sendable (inout State) throws -> T) rethrows -> T { - try _buffer.withUnsafeMutablePointers { state, lock in - _Lock.lock(lock) - return try withExtendedLifetime(state.pointee) { - defer { _Lock.unlock(lock) } - return try body(&state.pointee) - } - } - } -} - -extension LockedState where State == Void { - package init() { - self.init(initialState: ()) - } - - package func withLock(_ body: @Sendable () throws -> R) rethrows -> R { - return try withLock { _ in - try body() - } - } - - package func lock() { - _buffer.withUnsafeMutablePointerToElements { lock in - _Lock.lock(lock) - } - } - - package func unlock() { - _buffer.withUnsafeMutablePointerToElements { lock in - _Lock.unlock(lock) - } - } -} - -extension LockedState: @unchecked Sendable where State: Sendable {} - diff --git a/Sources/_Subprocess/Platforms/Subprocess+Darwin.swift b/Sources/_Subprocess/Platforms/Subprocess+Darwin.swift index 4ac2276e2..cd5c310aa 100644 --- a/Sources/_Subprocess/Platforms/Subprocess+Darwin.swift +++ b/Sources/_Subprocess/Platforms/Subprocess+Darwin.swift @@ -2,306 +2,144 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2024 Apple Inc. and the Swift.org project authors -// Licensed under Apache License v2.0 +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception // -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift.org project authors -// -// SPDX-License-Identifier: Apache-2.0 +// See https://swift.org/LICENSE.txt for license information // //===----------------------------------------------------------------------===// #if canImport(Darwin) -#if canImport(FoundationEssentials) -import FoundationEssentials -#elseif canImport(Foundation) -import Foundation -#endif - import Darwin -import Dispatch -import SystemPackage - -#if FOUNDATION_FRAMEWORK -@_implementationOnly import _FoundationCShims +internal import Dispatch +#if canImport(System) +import System #else -import _CShims +@preconcurrency import SystemPackage #endif -// Darwin specific implementation -extension Subprocess.Configuration { - internal typealias StringOrRawBytes = Subprocess.StringOrRawBytes +import _SubprocessCShims - internal func spawn( - withInput input: Subprocess.ExecutionInput, - output: Subprocess.ExecutionOutput, - error: Subprocess.ExecutionOutput - ) throws -> Subprocess { - let (executablePath, - env, argv, - intendedWorkingDir, - uidPtr, gidPtr, supplementaryGroups - ) = try self.preSpawn() - defer { - for ptr in env { ptr?.deallocate() } - for ptr in argv { ptr?.deallocate() } - uidPtr?.deallocate() - gidPtr?.deallocate() - } +#if SubprocessFoundation - // Setup file actions and spawn attributes - var fileActions: posix_spawn_file_actions_t? = nil - var spawnAttributes: posix_spawnattr_t? = nil - // Setup stdin, stdout, and stderr - posix_spawn_file_actions_init(&fileActions) - defer { - posix_spawn_file_actions_destroy(&fileActions) - } - // Input - var result: Int32 = -1 - if let inputRead = input.getReadFileDescriptor() { - result = posix_spawn_file_actions_adddup2(&fileActions, inputRead.rawValue, 0) - guard result == 0 else { - try self.cleanupAll(input: input, output: output, error: error) - throw POSIXError(.init(rawValue: result) ?? .ENODEV) - } - } - if let inputWrite = input.getWriteFileDescriptor() { - // Close parent side - result = posix_spawn_file_actions_addclose(&fileActions, inputWrite.rawValue) - guard result == 0 else { - try self.cleanupAll(input: input, output: output, error: error) - throw POSIXError(.init(rawValue: result) ?? .ENODEV) - } - } - // Output - if let outputWrite = output.getWriteFileDescriptor() { - result = posix_spawn_file_actions_adddup2(&fileActions, outputWrite.rawValue, 1) - guard result == 0 else { - try self.cleanupAll(input: input, output: output, error: error) - throw POSIXError(.init(rawValue: result) ?? .ENODEV) - } - } - if let outputRead = output.getReadFileDescriptor() { - // Close parent side - result = posix_spawn_file_actions_addclose(&fileActions, outputRead.rawValue) - guard result == 0 else { - try self.cleanupAll(input: input, output: output, error: error) - throw POSIXError(.init(rawValue: result) ?? .ENODEV) - } - } - // Error - if let errorWrite = error.getWriteFileDescriptor() { - result = posix_spawn_file_actions_adddup2(&fileActions, errorWrite.rawValue, 2) - guard result == 0 else { - try self.cleanupAll(input: input, output: output, error: error) - throw POSIXError(.init(rawValue: result) ?? .ENODEV) - } - } - if let errorRead = error.getReadFileDescriptor() { - // Close parent side - result = posix_spawn_file_actions_addclose(&fileActions, errorRead.rawValue) - guard result == 0 else { - try self.cleanupAll(input: input, output: output, error: error) - throw POSIXError(.init(rawValue: result) ?? .ENODEV) - } - } - // Setup spawnAttributes - posix_spawnattr_init(&spawnAttributes) - defer { - posix_spawnattr_destroy(&spawnAttributes) - } - var noSignals = sigset_t() - var allSignals = sigset_t() - sigemptyset(&noSignals) - sigfillset(&allSignals) - posix_spawnattr_setsigmask(&spawnAttributes, &noSignals) - posix_spawnattr_setsigdefault(&spawnAttributes, &allSignals) - // Configure spawnattr - var spawnAttributeError: Int32 = 0 - var flags: Int32 = POSIX_SPAWN_CLOEXEC_DEFAULT | - POSIX_SPAWN_SETSIGMASK | POSIX_SPAWN_SETSIGDEF - if let pgid = self.platformOptions.processGroupID { - flags |= POSIX_SPAWN_SETPGROUP - spawnAttributeError = posix_spawnattr_setpgroup(&spawnAttributes, pid_t(pgid)) - } - spawnAttributeError = posix_spawnattr_setflags(&spawnAttributes, Int16(flags)) - // Set QualityOfService - // spanattr_qos seems to only accept `QOS_CLASS_UTILITY` or `QOS_CLASS_BACKGROUND` - // and returns an error of `EINVAL` if anything else is provided - if spawnAttributeError == 0 && self.platformOptions.qualityOfService == .utility{ - spawnAttributeError = posix_spawnattr_set_qos_class_np(&spawnAttributes, QOS_CLASS_UTILITY) - } else if spawnAttributeError == 0 && self.platformOptions.qualityOfService == .background { - spawnAttributeError = posix_spawnattr_set_qos_class_np(&spawnAttributes, QOS_CLASS_BACKGROUND) - } - - // Setup cwd - var chdirError: Int32 = 0 - if intendedWorkingDir != .currentWorkingDirectory { - chdirError = intendedWorkingDir.withPlatformString { workDir in - return posix_spawn_file_actions_addchdir_np(&fileActions, workDir) - } - } - - // Error handling - if chdirError != 0 || spawnAttributeError != 0 { - try self.cleanupAll(input: input, output: output, error: error) - if spawnAttributeError != 0 { - throw POSIXError(.init(rawValue: result) ?? .ENODEV) - } +#if canImport(Darwin) +// On Darwin always prefer system Foundation +import Foundation +#else +// On other platforms prefer FoundationEssentials +import FoundationEssentials +#endif - if chdirError != 0 { - throw CocoaError(.fileNoSuchFile, userInfo: [ - .debugDescriptionErrorKey: "Cannot failed to change the working directory to \(intendedWorkingDir) with errno \(chdirError)" - ]) - } - } - // Run additional config - if let spawnConfig = self.platformOptions.preSpawnProcessConfigurator { - try spawnConfig(&spawnAttributes, &fileActions) - } - // Spawn - var pid: pid_t = 0 - let spawnError: CInt = executablePath.withCString { exePath in - return supplementaryGroups.withOptionalUnsafeBufferPointer { sgroups in - return _subprocess_spawn( - &pid, exePath, - &fileActions, &spawnAttributes, - argv, env, - uidPtr, gidPtr, - Int32(supplementaryGroups?.count ?? 0), sgroups?.baseAddress, - self.platformOptions.createSession ? 1 : 0 - ) - } - } - // Spawn error - if spawnError != 0 { - try self.cleanupAll(input: input, output: output, error: error) - throw POSIXError(.init(rawValue: spawnError) ?? .ENODEV) - } - return Subprocess( - processIdentifier: .init(value: pid), - executionInput: input, - executionOutput: output, - executionError: error - ) - } -} +#endif // SubprocessFoundation -// Special keys used in Error's user dictionary -extension String { - static let debugDescriptionErrorKey = "NSDebugDescription" -} +// MARK: - PlatformOptions -// MARK: - Platform Specific Options -extension Subprocess { - /// The collection of platform-specific settings - /// to configure the subprocess when running - public struct PlatformOptions: Sendable { - public var qualityOfService: QualityOfService = .default - /// Set user ID for the subprocess - public var userID: uid_t? = nil - /// Set the real and effective group ID and the saved - /// set-group-ID of the subprocess, equivalent to calling - /// `setgid()` on the child process. - /// Group ID is used to control permissions, particularly - /// for file access. - public var groupID: gid_t? = nil - /// Set list of supplementary group IDs for the subprocess - public var supplementaryGroups: [gid_t]? = nil - /// Set the process group for the subprocess, equivalent to - /// calling `setpgid()` on the child process. - /// Process group ID is used to group related processes for - /// controlling signals. - public var processGroupID: pid_t? = nil - /// Creates a session and sets the process group ID - /// i.e. Detach from the terminal. - public var createSession: Bool = false - /// A lightweight code requirement that you use to - /// evaluate the executable for a launching process. - public var launchRequirementData: Data? = nil - /// An ordered list of steps in order to tear down the child - /// process in case the parent task is cancelled before - /// the child proces terminates. - /// Always ends in sending a `.kill` signal at the end. - public var teardownSequence: [TeardownStep] = [] - /// A closure to configure platform-specific - /// spawning constructs. This closure enables direct - /// configuration or override of underlying platform-specific - /// spawn settings that `Subprocess` utilizes internally, - /// in cases where Subprocess does not provide higher-level - /// APIs for such modifications. - /// - /// On Darwin, Subprocess uses `posix_spawn()` as the - /// underlying spawning mechanism. This closure allows - /// modification of the `posix_spawnattr_t` spawn attribute - /// and file actions `posix_spawn_file_actions_t` before - /// they are sent to `posix_spawn()`. - public var preSpawnProcessConfigurator: ( +/// The collection of platform-specific settings +/// to configure the subprocess when running +public struct PlatformOptions: Sendable { + public var qualityOfService: QualityOfService = .default + /// Set user ID for the subprocess + public var userID: uid_t? = nil + /// Set the real and effective group ID and the saved + /// set-group-ID of the subprocess, equivalent to calling + /// `setgid()` on the child process. + /// Group ID is used to control permissions, particularly + /// for file access. + public var groupID: gid_t? = nil + /// Set list of supplementary group IDs for the subprocess + public var supplementaryGroups: [gid_t]? = nil + /// Set the process group for the subprocess, equivalent to + /// calling `setpgid()` on the child process. + /// Process group ID is used to group related processes for + /// controlling signals. + public var processGroupID: pid_t? = nil + /// Creates a session and sets the process group ID + /// i.e. Detach from the terminal. + public var createSession: Bool = false + /// An ordered list of steps in order to tear down the child + /// process in case the parent task is cancelled before + /// the child proces terminates. + /// Always ends in sending a `.kill` signal at the end. + public var teardownSequence: [TeardownStep] = [] + /// A closure to configure platform-specific + /// spawning constructs. This closure enables direct + /// configuration or override of underlying platform-specific + /// spawn settings that `Subprocess` utilizes internally, + /// in cases where Subprocess does not provide higher-level + /// APIs for such modifications. + /// + /// On Darwin, Subprocess uses `posix_spawn()` as the + /// underlying spawning mechanism. This closure allows + /// modification of the `posix_spawnattr_t` spawn attribute + /// and file actions `posix_spawn_file_actions_t` before + /// they are sent to `posix_spawn()`. + public var preSpawnProcessConfigurator: + ( @Sendable ( inout posix_spawnattr_t?, inout posix_spawn_file_actions_t? ) throws -> Void )? = nil - public init() {} - } + public init() {} } -extension Subprocess.PlatformOptions: Hashable { - public static func == (lhs: Subprocess.PlatformOptions, rhs: Subprocess.PlatformOptions) -> Bool { - // Since we can't compare closure equality, - // as long as preSpawnProcessConfigurator is set - // always returns false so that `PlatformOptions` - // with it set will never equal to each other - if lhs.preSpawnProcessConfigurator != nil || - rhs.preSpawnProcessConfigurator != nil { - return false - } - return lhs.qualityOfService == rhs.qualityOfService && - lhs.userID == rhs.userID && - lhs.groupID == rhs.groupID && - lhs.supplementaryGroups == rhs.supplementaryGroups && - lhs.processGroupID == rhs.processGroupID && - lhs.createSession == rhs.createSession && - lhs.launchRequirementData == rhs.launchRequirementData - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(self.qualityOfService) - hasher.combine(self.userID) - hasher.combine(self.groupID) - hasher.combine(self.supplementaryGroups) - hasher.combine(self.processGroupID) - hasher.combine(self.createSession) - hasher.combine(self.launchRequirementData) - // Since we can't really hash closures, - // use an UUID such that as long as - // `preSpawnProcessConfigurator` is set, it will - // never equal to other PlatformOptions - if self.preSpawnProcessConfigurator != nil { - hasher.combine(UUID()) - } +extension PlatformOptions { + #if SubprocessFoundation + public typealias QualityOfService = Foundation.QualityOfService + #else + /// Constants that indicate the nature and importance of work to the system. + /// + /// Work with higher quality of service classes receive more resources + /// than work with lower quality of service classes whenever + /// there’s resource contention. + public enum QualityOfService: Int, Sendable { + /// Used for work directly involved in providing an + /// interactive UI. For example, processing control + /// events or drawing to the screen. + case userInteractive = 0x21 + /// Used for performing work that has been explicitly requested + /// by the user, and for which results must be immediately + /// presented in order to allow for further user interaction. + /// For example, loading an email after a user has selected + /// it in a message list. + case userInitiated = 0x19 + /// Used for performing work which the user is unlikely to be + /// immediately waiting for the results. This work may have been + /// requested by the user or initiated automatically, and often + /// operates at user-visible timescales using a non-modal + /// progress indicator. For example, periodic content updates + /// or bulk file operations, such as media import. + case utility = 0x11 + /// Used for work that is not user initiated or visible. + /// In general, a user is unaware that this work is even happening. + /// For example, pre-fetching content, search indexing, backups, + /// or syncing of data with external systems. + case background = 0x09 + /// Indicates no explicit quality of service information. + /// Whenever possible, an appropriate quality of service is determined + /// from available sources. Otherwise, some quality of service level + /// between `.userInteractive` and `.utility` is used. + case `default` = -1 } + #endif } -extension Subprocess.PlatformOptions : CustomStringConvertible, CustomDebugStringConvertible { +extension PlatformOptions: CustomStringConvertible, CustomDebugStringConvertible { internal func description(withIndent indent: Int) -> String { let indent = String(repeating: " ", count: indent * 4) return """ -PlatformOptions( -\(indent) qualityOfService: \(self.qualityOfService), -\(indent) userID: \(String(describing: userID)), -\(indent) groupID: \(String(describing: groupID)), -\(indent) supplementaryGroups: \(String(describing: supplementaryGroups)), -\(indent) processGroupID: \(String(describing: processGroupID)), -\(indent) createSession: \(createSession), -\(indent) launchRequirementData: \(String(describing: launchRequirementData)), -\(indent) preSpawnProcessConfigurator: \(self.preSpawnProcessConfigurator == nil ? "not set" : "set") -\(indent)) -""" + PlatformOptions( + \(indent) qualityOfService: \(self.qualityOfService), + \(indent) userID: \(String(describing: userID)), + \(indent) groupID: \(String(describing: groupID)), + \(indent) supplementaryGroups: \(String(describing: supplementaryGroups)), + \(indent) processGroupID: \(String(describing: processGroupID)), + \(indent) createSession: \(createSession), + \(indent) preSpawnProcessConfigurator: \(self.preSpawnProcessConfigurator == nil ? "not set" : "set") + \(indent)) + """ } public var description: String { @@ -313,11 +151,243 @@ PlatformOptions( } } +// MARK: - Spawn +extension Configuration { + @available(macOS 15.0, *) // FIXME: manually added availability + internal func spawn< + Output: OutputProtocol, + Error: OutputProtocol + >( + withInput inputPipe: CreatedPipe, + output: Output, + outputPipe: CreatedPipe, + error: Error, + errorPipe: CreatedPipe + ) throws -> Execution { + // Instead of checking if every possible executable path + // is valid, spawn each directly and catch ENOENT + let possiblePaths = self.executable.possibleExecutablePaths( + withPathValue: self.environment.pathValue() + ) + return try self.preSpawn { args throws -> Execution in + let (env, uidPtr, gidPtr, supplementaryGroups) = args + for possibleExecutablePath in possiblePaths { + var pid: pid_t = 0 + + // Setup Arguments + let argv: [UnsafeMutablePointer?] = self.arguments.createArgs( + withExecutablePath: possibleExecutablePath + ) + defer { + for ptr in argv { ptr?.deallocate() } + } + + // Setup file actions and spawn attributes + var fileActions: posix_spawn_file_actions_t? = nil + var spawnAttributes: posix_spawnattr_t? = nil + // Setup stdin, stdout, and stderr + posix_spawn_file_actions_init(&fileActions) + defer { + posix_spawn_file_actions_destroy(&fileActions) + } + // Input + var result: Int32 = -1 + if let inputRead = inputPipe.readFileDescriptor { + result = posix_spawn_file_actions_adddup2(&fileActions, inputRead.wrapped.rawValue, 0) + guard result == 0 else { + try self.cleanupPreSpawn(input: inputPipe, output: outputPipe, error: errorPipe) + throw SubprocessError( + code: .init(.spawnFailed), + underlyingError: .init(rawValue: result) + ) + } + } + if let inputWrite = inputPipe.writeFileDescriptor { + // Close parent side + result = posix_spawn_file_actions_addclose(&fileActions, inputWrite.wrapped.rawValue) + guard result == 0 else { + try self.cleanupPreSpawn(input: inputPipe, output: outputPipe, error: errorPipe) + throw SubprocessError( + code: .init(.spawnFailed), + underlyingError: .init(rawValue: result) + ) + } + } + // Output + if let outputWrite = outputPipe.writeFileDescriptor { + result = posix_spawn_file_actions_adddup2(&fileActions, outputWrite.wrapped.rawValue, 1) + guard result == 0 else { + try self.cleanupPreSpawn(input: inputPipe, output: outputPipe, error: errorPipe) + throw SubprocessError( + code: .init(.spawnFailed), + underlyingError: .init(rawValue: result) + ) + } + } + if let outputRead = outputPipe.readFileDescriptor { + // Close parent side + result = posix_spawn_file_actions_addclose(&fileActions, outputRead.wrapped.rawValue) + guard result == 0 else { + try self.cleanupPreSpawn(input: inputPipe, output: outputPipe, error: errorPipe) + throw SubprocessError( + code: .init(.spawnFailed), + underlyingError: .init(rawValue: result) + ) + } + } + // Error + if let errorWrite = errorPipe.writeFileDescriptor { + result = posix_spawn_file_actions_adddup2(&fileActions, errorWrite.wrapped.rawValue, 2) + guard result == 0 else { + try self.cleanupPreSpawn(input: inputPipe, output: outputPipe, error: errorPipe) + throw SubprocessError( + code: .init(.spawnFailed), + underlyingError: .init(rawValue: result) + ) + } + } + if let errorRead = errorPipe.readFileDescriptor { + // Close parent side + result = posix_spawn_file_actions_addclose(&fileActions, errorRead.wrapped.rawValue) + guard result == 0 else { + try self.cleanupPreSpawn(input: inputPipe, output: outputPipe, error: errorPipe) + throw SubprocessError( + code: .init(.spawnFailed), + underlyingError: .init(rawValue: result) + ) + } + } + // Setup spawnAttributes + posix_spawnattr_init(&spawnAttributes) + defer { + posix_spawnattr_destroy(&spawnAttributes) + } + var noSignals = sigset_t() + var allSignals = sigset_t() + sigemptyset(&noSignals) + sigfillset(&allSignals) + posix_spawnattr_setsigmask(&spawnAttributes, &noSignals) + posix_spawnattr_setsigdefault(&spawnAttributes, &allSignals) + // Configure spawnattr + var spawnAttributeError: Int32 = 0 + var flags: Int32 = POSIX_SPAWN_CLOEXEC_DEFAULT | POSIX_SPAWN_SETSIGMASK | POSIX_SPAWN_SETSIGDEF + if let pgid = self.platformOptions.processGroupID { + flags |= POSIX_SPAWN_SETPGROUP + spawnAttributeError = posix_spawnattr_setpgroup(&spawnAttributes, pid_t(pgid)) + } + spawnAttributeError = posix_spawnattr_setflags(&spawnAttributes, Int16(flags)) + // Set QualityOfService + // spanattr_qos seems to only accept `QOS_CLASS_UTILITY` or `QOS_CLASS_BACKGROUND` + // and returns an error of `EINVAL` if anything else is provided + if spawnAttributeError == 0 && self.platformOptions.qualityOfService == .utility { + spawnAttributeError = posix_spawnattr_set_qos_class_np(&spawnAttributes, QOS_CLASS_UTILITY) + } else if spawnAttributeError == 0 && self.platformOptions.qualityOfService == .background { + spawnAttributeError = posix_spawnattr_set_qos_class_np(&spawnAttributes, QOS_CLASS_BACKGROUND) + } + + // Setup cwd + let intendedWorkingDir = self.workingDirectory.string + let chdirError: Int32 = intendedWorkingDir.withPlatformString { workDir in + return posix_spawn_file_actions_addchdir_np(&fileActions, workDir) + } + + // Error handling + if chdirError != 0 || spawnAttributeError != 0 { + try self.cleanupPreSpawn(input: inputPipe, output: outputPipe, error: errorPipe) + if spawnAttributeError != 0 { + throw SubprocessError( + code: .init(.spawnFailed), + underlyingError: .init(rawValue: spawnAttributeError) + ) + } + + if chdirError != 0 { + throw SubprocessError( + code: .init(.spawnFailed), + underlyingError: .init(rawValue: spawnAttributeError) + ) + } + } + // Run additional config + if let spawnConfig = self.platformOptions.preSpawnProcessConfigurator { + try spawnConfig(&spawnAttributes, &fileActions) + } + + // Spawn + let spawnError: CInt = possibleExecutablePath.withCString { exePath in + return supplementaryGroups.withOptionalUnsafeBufferPointer { sgroups in + return _subprocess_spawn( + &pid, + exePath, + &fileActions, + &spawnAttributes, + argv, + env, + uidPtr, + gidPtr, + Int32(supplementaryGroups?.count ?? 0), + sgroups?.baseAddress, + self.platformOptions.createSession ? 1 : 0 + ) + } + } + // Spawn error + if spawnError != 0 { + if spawnError == ENOENT { + // Move on to another possible path + continue + } + // Throw all other errors + try self.cleanupPreSpawn( + input: inputPipe, + output: outputPipe, + error: errorPipe + ) + throw SubprocessError( + code: .init(.spawnFailed), + underlyingError: .init(rawValue: spawnError) + ) + } + return Execution( + processIdentifier: .init(value: pid), + output: output, + error: error, + outputPipe: outputPipe, + errorPipe: errorPipe + ) + } + + // If we reach this point, it means either the executable path + // or working directory is not valid. Since posix_spawn does not + // provide which one is not valid, here we make a best effort guess + // by checking whether the working directory is valid. This technically + // still causes TOUTOC issue, but it's the best we can do for error recovery. + try self.cleanupPreSpawn(input: inputPipe, output: outputPipe, error: errorPipe) + let workingDirectory = self.workingDirectory.string + guard Configuration.pathAccessible(workingDirectory, mode: F_OK) else { + throw SubprocessError( + code: .init(.failedToChangeWorkingDirectory(workingDirectory)), + underlyingError: .init(rawValue: ENOENT) + ) + } + throw SubprocessError( + code: .init(.executableNotFound(self.executable.description)), + underlyingError: .init(rawValue: ENOENT) + ) + } + } +} + +// Special keys used in Error's user dictionary +extension String { + static let debugDescriptionErrorKey = "NSDebugDescription" +} + // MARK: - Process Monitoring @Sendable internal func monitorProcessTermination( - forProcessWithIdentifier pid: Subprocess.ProcessIdentifier -) async throws -> Subprocess.TerminationStatus { + forProcessWithIdentifier pid: ProcessIdentifier +) async throws -> TerminationStatus { return try await withCheckedThrowingContinuation { continuation in let source = DispatchSource.makeProcessSource( identifier: pid.value, @@ -329,7 +399,12 @@ internal func monitorProcessTermination( var siginfo = siginfo_t() let rc = waitid(P_PID, id_t(pid.value), &siginfo, WEXITED) guard rc == 0 else { - continuation.resume(throwing: POSIXError(.init(rawValue: errno) ?? .ENODEV)) + continuation.resume( + throwing: SubprocessError( + code: .init(.failedToMonitorProcess), + underlyingError: .init(rawValue: errno) + ) + ) return } switch siginfo.si_code { @@ -350,4 +425,4 @@ internal func monitorProcessTermination( } } -#endif // canImport(Darwin) +#endif // canImport(Darwin) diff --git a/Sources/_Subprocess/Platforms/Subprocess+Linux.swift b/Sources/_Subprocess/Platforms/Subprocess+Linux.swift index 9debf2e39..23c9b36e5 100644 --- a/Sources/_Subprocess/Platforms/Subprocess+Linux.swift +++ b/Sources/_Subprocess/Platforms/Subprocess+Linux.swift @@ -2,196 +2,210 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2024 Apple Inc. and the Swift.org project authors -// Licensed under Apache License v2.0 +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception // -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift.org project authors -// -// SPDX-License-Identifier: Apache-2.0 +// See https://swift.org/LICENSE.txt for license information // //===----------------------------------------------------------------------===// -#if canImport(Glibc) +#if canImport(Glibc) || canImport(Bionic) || canImport(Musl) + +#if canImport(System) +import System +#else +@preconcurrency import SystemPackage +#endif +#if canImport(Glibc) import Glibc -import Dispatch -import SystemPackage -import FoundationEssentials -import _CShims +#elseif canImport(Bionic) +import Bionic +#elseif canImport(Musl) +import Musl +#endif -// Linux specific implementations -extension Subprocess.Configuration { - internal typealias StringOrRawBytes = Subprocess.StringOrRawBytes +internal import Dispatch - internal func spawn( - withInput input: Subprocess.ExecutionInput, - output: Subprocess.ExecutionOutput, - error: Subprocess.ExecutionOutput - ) throws -> Subprocess { +import Synchronization +import _SubprocessCShims + +// Linux specific implementations +extension Configuration { + internal func spawn< + Output: OutputProtocol, + Error: OutputProtocol + >( + withInput inputPipe: CreatedPipe, + output: Output, + outputPipe: CreatedPipe, + error: Error, + errorPipe: CreatedPipe + ) throws -> Execution { _setupMonitorSignalHandler() - let (executablePath, - env, argv, - intendedWorkingDir, - uidPtr, gidPtr, - supplementaryGroups - ) = try self.preSpawn() - var processGroupIDPtr: UnsafeMutablePointer? = nil - if let processGroupID = self.platformOptions.processGroupID { - processGroupIDPtr = .allocate(capacity: 1) - processGroupIDPtr?.pointee = gid_t(processGroupID) - } - defer { - for ptr in env { ptr?.deallocate() } - for ptr in argv { ptr?.deallocate() } - uidPtr?.deallocate() - gidPtr?.deallocate() - processGroupIDPtr?.deallocate() - } + // Instead of checking if every possible executable path + // is valid, spawn each directly and catch ENOENT + let possiblePaths = self.executable.possibleExecutablePaths( + withPathValue: self.environment.pathValue() + ) - let fileDescriptors: [CInt] = [ - input.getReadFileDescriptor()?.rawValue ?? -1, - input.getWriteFileDescriptor()?.rawValue ?? -1, - output.getWriteFileDescriptor()?.rawValue ?? -1, - output.getReadFileDescriptor()?.rawValue ?? -1, - error.getWriteFileDescriptor()?.rawValue ?? -1, - error.getReadFileDescriptor()?.rawValue ?? -1 - ] + return try self.preSpawn { args throws -> Execution in + let (env, uidPtr, gidPtr, supplementaryGroups) = args - var workingDirectory: String? - if intendedWorkingDir != FilePath.currentWorkingDirectory { - // Only pass in working directory if it's different - workingDirectory = intendedWorkingDir.string - } - // Spawn - var pid: pid_t = 0 - let spawnError: CInt = executablePath.withCString { exePath in - return workingDirectory.withOptionalCString { workingDir in - return supplementaryGroups.withOptionalUnsafeBufferPointer { sgroups in - return fileDescriptors.withUnsafeBufferPointer { fds in - return _subprocess_fork_exec( - &pid, exePath, workingDir, - fds.baseAddress!, - argv, env, - uidPtr, gidPtr, - processGroupIDPtr, - CInt(supplementaryGroups?.count ?? 0), sgroups?.baseAddress, - self.platformOptions.createSession ? 1 : 0, - self.platformOptions.preSpawnProcessConfigurator - ) + for possibleExecutablePath in possiblePaths { + var processGroupIDPtr: UnsafeMutablePointer? = nil + if let processGroupID = self.platformOptions.processGroupID { + processGroupIDPtr = .allocate(capacity: 1) + processGroupIDPtr?.pointee = gid_t(processGroupID) + } + // Setup Arguments + let argv: [UnsafeMutablePointer?] = self.arguments.createArgs( + withExecutablePath: possibleExecutablePath + ) + defer { + for ptr in argv { ptr?.deallocate() } + } + // Setup input + let fileDescriptors: [CInt] = [ + inputPipe.readFileDescriptor?.wrapped.rawValue ?? -1, + inputPipe.writeFileDescriptor?.wrapped.rawValue ?? -1, + outputPipe.writeFileDescriptor?.wrapped.rawValue ?? -1, + outputPipe.readFileDescriptor?.wrapped.rawValue ?? -1, + errorPipe.writeFileDescriptor?.wrapped.rawValue ?? -1, + errorPipe.readFileDescriptor?.wrapped.rawValue ?? -1, + ] + + let workingDirectory: String = self.workingDirectory.string + // Spawn + var pid: pid_t = 0 + let spawnError: CInt = possibleExecutablePath.withCString { exePath in + return workingDirectory.withCString { workingDir in + return supplementaryGroups.withOptionalUnsafeBufferPointer { sgroups in + return fileDescriptors.withUnsafeBufferPointer { fds in + return _subprocess_fork_exec( + &pid, + exePath, + workingDir, + fds.baseAddress!, + argv, + env, + uidPtr, + gidPtr, + processGroupIDPtr, + CInt(supplementaryGroups?.count ?? 0), + sgroups?.baseAddress, + self.platformOptions.createSession ? 1 : 0, + self.platformOptions.preSpawnProcessConfigurator + ) + } + } } } + // Spawn error + if spawnError != 0 { + if spawnError == ENOENT { + // Move on to another possible path + continue + } + // Throw all other errors + try self.cleanupPreSpawn( + input: inputPipe, + output: outputPipe, + error: errorPipe + ) + throw SubprocessError( + code: .init(.spawnFailed), + underlyingError: .init(rawValue: spawnError) + ) + } + return Execution( + processIdentifier: .init(value: pid), + output: output, + error: error, + outputPipe: outputPipe, + errorPipe: errorPipe + ) } + + // If we reach this point, it means either the executable path + // or working directory is not valid. Since posix_spawn does not + // provide which one is not valid, here we make a best effort guess + // by checking whether the working directory is valid. This technically + // still causes TOUTOC issue, but it's the best we can do for error recovery. + try self.cleanupPreSpawn(input: inputPipe, output: outputPipe, error: errorPipe) + let workingDirectory = self.workingDirectory.string + guard Configuration.pathAccessible(workingDirectory, mode: F_OK) else { + throw SubprocessError( + code: .init(.failedToChangeWorkingDirectory(workingDirectory)), + underlyingError: .init(rawValue: ENOENT) + ) + } + throw SubprocessError( + code: .init(.executableNotFound(self.executable.description)), + underlyingError: .init(rawValue: ENOENT) + ) } - // Spawn error - if spawnError != 0 { - try self.cleanupAll(input: input, output: output, error: error) - throw POSIXError(.init(rawValue: spawnError) ?? .ENODEV) - } - return Subprocess( - processIdentifier: .init(value: pid), - executionInput: input, - executionOutput: output, - executionError: error - ) } } // MARK: - Platform Specific Options -extension Subprocess { - /// The collection of platform-specific settings - /// to configure the subprocess when running - public struct PlatformOptions: Sendable { - // Set user ID for the subprocess - public var userID: uid_t? = nil - /// Set the real and effective group ID and the saved - /// set-group-ID of the subprocess, equivalent to calling - /// `setgid()` on the child process. - /// Group ID is used to control permissions, particularly - /// for file access. - public var groupID: gid_t? = nil - // Set list of supplementary group IDs for the subprocess - public var supplementaryGroups: [gid_t]? = nil - /// Set the process group for the subprocess, equivalent to - /// calling `setpgid()` on the child process. - /// Process group ID is used to group related processes for - /// controlling signals. - public var processGroupID: pid_t? = nil - // Creates a session and sets the process group ID - // i.e. Detach from the terminal. - public var createSession: Bool = false - /// An ordered list of steps in order to tear down the child - /// process in case the parent task is cancelled before - /// the child proces terminates. - /// Always ends in sending a `.kill` signal at the end. - public var teardownSequence: [TeardownStep] = [] - /// A closure to configure platform-specific - /// spawning constructs. This closure enables direct - /// configuration or override of underlying platform-specific - /// spawn settings that `Subprocess` utilizes internally, - /// in cases where Subprocess does not provide higher-level - /// APIs for such modifications. - /// - /// On Linux, Subprocess uses `fork/exec` as the - /// underlying spawning mechanism. This closure is called - /// after `fork()` but before `exec()`. You may use it to - /// call any necessary process setup functions. - public var preSpawnProcessConfigurator: (@convention(c) @Sendable () -> Void)? = nil - public init() {} - } -} - -extension Subprocess.PlatformOptions: Hashable { - public static func ==( - lhs: Subprocess.PlatformOptions, - rhs: Subprocess.PlatformOptions - ) -> Bool { - // Since we can't compare closure equality, - // as long as preSpawnProcessConfigurator is set - // always returns false so that `PlatformOptions` - // with it set will never equal to each other - if lhs.preSpawnProcessConfigurator != nil || - rhs.preSpawnProcessConfigurator != nil { - return false - } - return lhs.userID == rhs.userID && - lhs.groupID == rhs.groupID && - lhs.supplementaryGroups == rhs.supplementaryGroups && - lhs.processGroupID == rhs.processGroupID && - lhs.createSession == rhs.createSession - } +/// The collection of platform-specific settings +/// to configure the subprocess when running +public struct PlatformOptions: Sendable { + // Set user ID for the subprocess + public var userID: uid_t? = nil + /// Set the real and effective group ID and the saved + /// set-group-ID of the subprocess, equivalent to calling + /// `setgid()` on the child process. + /// Group ID is used to control permissions, particularly + /// for file access. + public var groupID: gid_t? = nil + // Set list of supplementary group IDs for the subprocess + public var supplementaryGroups: [gid_t]? = nil + /// Set the process group for the subprocess, equivalent to + /// calling `setpgid()` on the child process. + /// Process group ID is used to group related processes for + /// controlling signals. + public var processGroupID: pid_t? = nil + // Creates a session and sets the process group ID + // i.e. Detach from the terminal. + public var createSession: Bool = false + /// An ordered list of steps in order to tear down the child + /// process in case the parent task is cancelled before + /// the child proces terminates. + /// Always ends in sending a `.kill` signal at the end. + public var teardownSequence: [TeardownStep] = [] + /// A closure to configure platform-specific + /// spawning constructs. This closure enables direct + /// configuration or override of underlying platform-specific + /// spawn settings that `Subprocess` utilizes internally, + /// in cases where Subprocess does not provide higher-level + /// APIs for such modifications. + /// + /// On Linux, Subprocess uses `fork/exec` as the + /// underlying spawning mechanism. This closure is called + /// after `fork()` but before `exec()`. You may use it to + /// call any necessary process setup functions. + public var preSpawnProcessConfigurator: (@convention(c) @Sendable () -> Void)? = nil - public func hash(into hasher: inout Hasher) { - hasher.combine(userID) - hasher.combine(groupID) - hasher.combine(supplementaryGroups) - hasher.combine(processGroupID) - hasher.combine(createSession) - // Since we can't really hash closures, - // use an UUID such that as long as - // `preSpawnProcessConfigurator` is set, it will - // never equal to other PlatformOptions - if self.preSpawnProcessConfigurator != nil { - hasher.combine(UUID()) - } - } + public init() {} } -extension Subprocess.PlatformOptions : CustomStringConvertible, CustomDebugStringConvertible { +extension PlatformOptions: CustomStringConvertible, CustomDebugStringConvertible { internal func description(withIndent indent: Int) -> String { let indent = String(repeating: " ", count: indent * 4) return """ -PlatformOptions( -\(indent) userID: \(String(describing: userID)), -\(indent) groupID: \(String(describing: groupID)), -\(indent) supplementaryGroups: \(String(describing: supplementaryGroups)), -\(indent) processGroupID: \(String(describing: processGroupID)), -\(indent) createSession: \(createSession), -\(indent) preSpawnProcessConfigurator: \(self.preSpawnProcessConfigurator == nil ? "not set" : "set") -\(indent)) -""" + PlatformOptions( + \(indent) userID: \(String(describing: userID)), + \(indent) groupID: \(String(describing: groupID)), + \(indent) supplementaryGroups: \(String(describing: supplementaryGroups)), + \(indent) processGroupID: \(String(describing: processGroupID)), + \(indent) createSession: \(createSession), + \(indent) preSpawnProcessConfigurator: \(self.preSpawnProcessConfigurator == nil ? "not set" : "set") + \(indent)) + """ } public var description: String { @@ -211,12 +225,13 @@ extension String { // MARK: - Process Monitoring @Sendable internal func monitorProcessTermination( - forProcessWithIdentifier pid: Subprocess.ProcessIdentifier -) async throws -> Subprocess.TerminationStatus { + forProcessWithIdentifier pid: ProcessIdentifier +) async throws -> TerminationStatus { return try await withCheckedThrowingContinuation { continuation in _childProcessContinuations.withLock { continuations in if let existing = continuations.removeValue(forKey: pid.value), - case .status(let existingStatus) = existing { + case .status(let existingStatus) = existing + { // We already have existing status to report continuation.resume(returning: existingStatus) } else { @@ -228,28 +243,26 @@ internal func monitorProcessTermination( } private enum ContinuationOrStatus { - case continuation(CheckedContinuation) - case status(Subprocess.TerminationStatus) + case continuation(CheckedContinuation) + case status(TerminationStatus) } -private let _childProcessContinuations: LockedState< - [pid_t: ContinuationOrStatus] -> = LockedState(initialState: [:]) +private let _childProcessContinuations: + Mutex< + [pid_t: ContinuationOrStatus] + > = Mutex([:]) + +private let signalSource: SendableSourceSignal = SendableSourceSignal() -private var signalSource: (any DispatchSourceSignal)? = nil private let setup: () = { - signalSource = DispatchSource.makeSignalSource( - signal: SIGCHLD, - queue: .global() - ) - signalSource?.setEventHandler { + signalSource.setEventHandler { _childProcessContinuations.withLock { continuations in while true { var siginfo = siginfo_t() guard waitid(P_ALL, id_t(0), &siginfo, WEXITED) == 0 else { return } - var status: Subprocess.TerminationStatus? = nil + var status: TerminationStatus? = nil switch siginfo.si_code { case .init(CLD_EXITED): status = .exited(siginfo._sifields._sigchld.si_status) @@ -265,7 +278,8 @@ private let setup: () = { if let status = status { let pid = siginfo._sifields._sigchld.si_pid if let existing = continuations.removeValue(forKey: pid), - case .continuation(let c) = existing { + case .continuation(let c) = existing + { c.resume(returning: status) } else { // We don't have continuation yet, just state status @@ -275,13 +289,33 @@ private let setup: () = { } } } - signalSource?.resume() + signalSource.resume() }() +/// Unchecked Sendable here since this class is only explicitly +/// initialzied once during the lifetime of the process +final class SendableSourceSignal: @unchecked Sendable { + private let signalSource: DispatchSourceSignal + + func setEventHandler(handler: @escaping DispatchSourceHandler) { + self.signalSource.setEventHandler(handler: handler) + } + + func resume() { + self.signalSource.resume() + } + + init() { + self.signalSource = DispatchSource.makeSignalSource( + signal: SIGCHLD, + queue: .global() + ) + } +} + private func _setupMonitorSignalHandler() { // Only executed once setup } -#endif // canImport(Glibc) - +#endif // canImport(Glibc) || canImport(Bionic) || canImport(Musl) diff --git a/Sources/_Subprocess/Platforms/Subprocess+Unix.swift b/Sources/_Subprocess/Platforms/Subprocess+Unix.swift index bd1103010..ae8fd639b 100644 --- a/Sources/_Subprocess/Platforms/Subprocess+Unix.swift +++ b/Sources/_Subprocess/Platforms/Subprocess+Unix.swift @@ -2,119 +2,143 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2024 Apple Inc. and the Swift.org project authors -// Licensed under Apache License v2.0 +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception // -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift.org project authors -// -// SPDX-License-Identifier: Apache-2.0 +// See https://swift.org/LICENSE.txt for license information // //===----------------------------------------------------------------------===// -#if canImport(Darwin) || canImport(Glibc) +#if canImport(Darwin) || canImport(Glibc) || canImport(Bionic) || canImport(Musl) -#if canImport(FoundationEssentials) -import FoundationEssentials -#elseif canImport(Foundation) -import Foundation +#if canImport(System) +import System +#else +@preconcurrency import SystemPackage #endif +import _SubprocessCShims + #if canImport(Darwin) import Darwin +#elseif canImport(Bionic) +import Bionic #elseif canImport(Glibc) import Glibc +#elseif canImport(Musl) +import Musl #endif -#if FOUNDATION_FRAMEWORK -@_implementationOnly import _FoundationCShims -#else -import _CShims -#endif - -import Dispatch -import SystemPackage +package import Dispatch // MARK: - Signals -extension Subprocess { - /// Signals are standardized messages sent to a running program - /// to trigger specific behavior, such as quitting or error handling. - public struct Signal : Hashable, Sendable { - /// The underlying platform specific value for the signal - public let rawValue: Int32 - - private init(rawValue: Int32) { - self.rawValue = rawValue - } - /// The `.interrupt` signal is sent to a process by its - /// controlling terminal when a user wishes to interrupt - /// the process. - public static var interrupt: Self { .init(rawValue: SIGINT) } - /// The `.terminate` signal is sent to a process to request its - /// termination. Unlike the `.kill` signal, it can be caught - /// and interpreted or ignored by the process. This allows - /// the process to perform nice termination releasing resources - /// and saving state if appropriate. `.interrupt` is nearly - /// identical to `.terminate`. - public static var terminate: Self { .init(rawValue: SIGTERM) } - /// The `.suspend` signal instructs the operating system - /// to stop a process for later resumption. - public static var suspend: Self { .init(rawValue: SIGSTOP) } - /// The `resume` signal instructs the operating system to - /// continue (restart) a process previously paused by the - /// `.suspend` signal. - public static var resume: Self { .init(rawValue: SIGCONT) } - /// The `.kill` signal is sent to a process to cause it to - /// terminate immediately (kill). In contrast to `.terminate` - /// and `.interrupt`, this signal cannot be caught or ignored, - /// and the receiving process cannot perform any - /// clean-up upon receiving this signal. - public static var kill: Self { .init(rawValue: SIGKILL) } - /// The `.terminalClosed` signal is sent to a process when - /// its controlling terminal is closed. In modern systems, - /// this signal usually means that the controlling pseudo - /// or virtual terminal has been closed. - public static var terminalClosed: Self { .init(rawValue: SIGHUP) } - /// The `.quit` signal is sent to a process by its controlling - /// terminal when the user requests that the process quit - /// and perform a core dump. - public static var quit: Self { .init(rawValue: SIGQUIT) } - /// The `.userDefinedOne` signal is sent to a process to indicate - /// user-defined conditions. - public static var userDefinedOne: Self { .init(rawValue: SIGUSR1) } - /// The `.userDefinedTwo` signal is sent to a process to indicate - /// user-defined conditions. - public static var userDefinedTwo: Self { .init(rawValue: SIGUSR2) } - /// The `.alarm` signal is sent to a process when the corresponding - /// time limit is reached. - public static var alarm: Self { .init(rawValue: SIGALRM) } - /// The `.windowSizeChange` signal is sent to a process when - /// its controlling terminal changes its size (a window change). - public static var windowSizeChange: Self { .init(rawValue: SIGWINCH) } +/// Signals are standardized messages sent to a running program +/// to trigger specific behavior, such as quitting or error handling. +public struct Signal: Hashable, Sendable { + /// The underlying platform specific value for the signal + public let rawValue: Int32 + + private init(rawValue: Int32) { + self.rawValue = rawValue + } + + /// The `.interrupt` signal is sent to a process by its + /// controlling terminal when a user wishes to interrupt + /// the process. + public static var interrupt: Self { .init(rawValue: SIGINT) } + /// The `.terminate` signal is sent to a process to request its + /// termination. Unlike the `.kill` signal, it can be caught + /// and interpreted or ignored by the process. This allows + /// the process to perform nice termination releasing resources + /// and saving state if appropriate. `.interrupt` is nearly + /// identical to `.terminate`. + public static var terminate: Self { .init(rawValue: SIGTERM) } + /// The `.suspend` signal instructs the operating system + /// to stop a process for later resumption. + public static var suspend: Self { .init(rawValue: SIGSTOP) } + /// The `resume` signal instructs the operating system to + /// continue (restart) a process previously paused by the + /// `.suspend` signal. + public static var resume: Self { .init(rawValue: SIGCONT) } + /// The `.kill` signal is sent to a process to cause it to + /// terminate immediately (kill). In contrast to `.terminate` + /// and `.interrupt`, this signal cannot be caught or ignored, + /// and the receiving process cannot perform any + /// clean-up upon receiving this signal. + public static var kill: Self { .init(rawValue: SIGKILL) } + /// The `.terminalClosed` signal is sent to a process when + /// its controlling terminal is closed. In modern systems, + /// this signal usually means that the controlling pseudo + /// or virtual terminal has been closed. + public static var terminalClosed: Self { .init(rawValue: SIGHUP) } + /// The `.quit` signal is sent to a process by its controlling + /// terminal when the user requests that the process quit + /// and perform a core dump. + public static var quit: Self { .init(rawValue: SIGQUIT) } + /// The `.userDefinedOne` signal is sent to a process to indicate + /// user-defined conditions. + public static var userDefinedOne: Self { .init(rawValue: SIGUSR1) } + /// The `.userDefinedTwo` signal is sent to a process to indicate + /// user-defined conditions. + public static var userDefinedTwo: Self { .init(rawValue: SIGUSR2) } + /// The `.alarm` signal is sent to a process when the corresponding + /// time limit is reached. + public static var alarm: Self { .init(rawValue: SIGALRM) } + /// The `.windowSizeChange` signal is sent to a process when + /// its controlling terminal changes its size (a window change). + public static var windowSizeChange: Self { .init(rawValue: SIGWINCH) } +} + +// MARK: - ProcessIdentifier + +/// A platform independent identifier for a Subprocess. +public struct ProcessIdentifier: Sendable, Hashable, Codable { + /// The platform specific process identifier value + public let value: pid_t + + public init(value: pid_t) { + self.value = value } +} +extension ProcessIdentifier: CustomStringConvertible, CustomDebugStringConvertible { + public var description: String { "\(self.value)" } + + public var debugDescription: String { "\(self.value)" } +} + +@available(macOS 15.0, *) // FIXME: manually added availability +extension Execution { /// Send the given signal to the child process. /// - Parameters: /// - signal: The signal to send. /// - shouldSendToProcessGroup: Whether this signal should be sent to /// the entire process group. - public func send(_ signal: Signal, toProcessGroup shouldSendToProcessGroup: Bool) throws { + public func send( + signal: Signal, + toProcessGroup shouldSendToProcessGroup: Bool = false + ) throws { let pid = shouldSendToProcessGroup ? -(self.processIdentifier.value) : self.processIdentifier.value guard kill(pid, signal.rawValue) == 0 else { - throw POSIXError(.init(rawValue: errno)!) + throw SubprocessError( + code: .init(.failedToSendSignal(signal.rawValue)), + underlyingError: .init(rawValue: errno) + ) } } - internal func tryTerminate() -> Error? { + internal func tryTerminate() -> Swift.Error? { do { - try self.send(.kill, toProcessGroup: true) + try self.send(signal: .kill) } catch { - guard let posixError: POSIXError = error as? POSIXError else { + guard let posixError: SubprocessError = error as? SubprocessError else { return error } // Ignore ESRCH (no such process) - if posixError.code != .ESRCH { + if let underlyingError = posixError.underlyingError, + underlyingError.rawValue != ESRCH + { return error } } @@ -123,21 +147,32 @@ extension Subprocess { } // MARK: - Environment Resolution -extension Subprocess.Environment { - internal static let pathEnvironmentVariableName = "PATH" +extension Environment { + internal static let pathVariableName = "PATH" internal func pathValue() -> String? { switch self.config { case .inherit(let overrides): // If PATH value exists in overrides, use it - if let value = overrides[.string(Self.pathEnvironmentVariableName)] { - return value.stringValue + if let value = overrides[Self.pathVariableName] { + return value } // Fall back to current process - return ProcessInfo.processInfo.environment[Self.pathEnvironmentVariableName] + return Self.currentEnvironmentValues()[Self.pathVariableName] case .custom(let fullEnvironment): - if let value = fullEnvironment[.string(Self.pathEnvironmentVariableName)] { - return value.stringValue + if let value = fullEnvironment[Self.pathVariableName] { + return value + } + return nil + case .rawBytes(let rawBytesArray): + let needle: [UInt8] = Array("\(Self.pathVariableName)=".utf8) + for row in rawBytesArray { + guard row.starts(with: needle) else { + continue + } + // Attempt to + let pathValue = row.dropFirst(needle.count) + return String(decoding: pathValue, as: UTF8.self) } return nil } @@ -147,8 +182,8 @@ extension Subprocess.Environment { // manually deallocated internal func createEnv() -> [UnsafeMutablePointer?] { func createFullCString( - fromKey keyContainer: Subprocess.StringOrRawBytes, - value valueContainer: Subprocess.StringOrRawBytes + fromKey keyContainer: StringOrRawBytes, + value valueContainer: StringOrRawBytes ) -> UnsafeMutablePointer { let rawByteKey: UnsafeMutablePointer = keyContainer.createRawBytes() let rawByteValue: UnsafeMutablePointer = valueContainer.createRawBytes() @@ -170,21 +205,12 @@ extension Subprocess.Environment { var env: [UnsafeMutablePointer?] = [] switch self.config { case .inherit(let updates): - var current = ProcessInfo.processInfo.environment - for (keyContainer, valueContainer) in updates { - if let stringKey = keyContainer.stringValue { - // Remove the value from current to override it - current.removeValue(forKey: stringKey) - } - // Fast path - if case .string(let stringKey) = keyContainer, - case .string(let stringValue) = valueContainer { - let fullString = "\(stringKey)=\(stringValue)" - env.append(strdup(fullString)) - continue - } - - env.append(createFullCString(fromKey: keyContainer, value: valueContainer)) + var current = Self.currentEnvironmentValues() + for (key, value) in updates { + // Remove the value from current to override it + current.removeValue(forKey: key) + let fullString = "\(key)=\(value)" + env.append(strdup(fullString)) } // Add the rest of `current` to env for (key, value) in current { @@ -192,24 +218,43 @@ extension Subprocess.Environment { env.append(strdup(fullString)) } case .custom(let customValues): - for (keyContainer, valueContainer) in customValues { - // Fast path - if case .string(let stringKey) = keyContainer, - case .string(let stringValue) = valueContainer { - let fullString = "\(stringKey)=\(stringValue)" - env.append(strdup(fullString)) - continue - } - env.append(createFullCString(fromKey: keyContainer, value: valueContainer)) + for (key, value) in customValues { + let fullString = "\(key)=\(value)" + env.append(strdup(fullString)) + } + case .rawBytes(let rawBytesArray): + for rawBytes in rawBytesArray { + env.append(strdup(rawBytes)) } } env.append(nil) return env } + + internal static func withCopiedEnv(_ body: ([UnsafeMutablePointer]) -> R) -> R { + var values: [UnsafeMutablePointer] = [] + // This lock is taken by calls to getenv, so we want as few callouts to other code as possible here. + _subprocess_lock_environ() + guard + let environments: UnsafeMutablePointer?> = + _subprocess_get_environ() + else { + _subprocess_unlock_environ() + return body([]) + } + var curr = environments + while let value = curr.pointee { + values.append(strdup(value)) + curr = curr.advanced(by: 1) + } + _subprocess_unlock_environ() + defer { values.forEach { free($0) } } + return body(values) + } } // MARK: Args Creation -extension Subprocess.Arguments { +extension Arguments { // This method follows the standard "create" rule: `args` needs to be // manually deallocated internal func createArgs(withExecutablePath executablePath: String) -> [UnsafeMutablePointer?] { @@ -225,42 +270,23 @@ extension Subprocess.Arguments { } } -// MARK: - ProcessIdentifier -extension Subprocess { - /// A platform independent identifier for a subprocess. - public struct ProcessIdentifier: Sendable, Hashable, Codable { - /// The platform specific process identifier value - public let value: pid_t - - public init(value: pid_t) { - self.value = value - } - } -} - -extension Subprocess.ProcessIdentifier : CustomStringConvertible, CustomDebugStringConvertible { - public var description: String { "\(self.value)" } - - public var debugDescription: String { "\(self.value)" } -} - // MARK: - Executable Searching -extension Subprocess.Executable { +extension Executable { internal static var defaultSearchPaths: Set { return Set([ "/usr/bin", "/bin", "/usr/sbin", "/sbin", - "/usr/local/bin" + "/usr/local/bin", ]) } - internal func resolveExecutablePath(withPathValue pathValue: String?) -> String? { + internal func resolveExecutablePath(withPathValue pathValue: String?) throws -> String { switch self.storage { case .executable(let executableName): // If the executableName in is already a full path, return it directly - if Subprocess.Configuration.pathAccessible(executableName, mode: X_OK) { + if Configuration.pathAccessible(executableName, mode: X_OK) { return executableName } // Get $PATH from environment @@ -274,50 +300,38 @@ extension Subprocess.Executable { for path in searchPaths { let fullPath = "\(path)/\(executableName)" - let fileExists = Subprocess.Configuration.pathAccessible(fullPath, mode: X_OK) + let fileExists = Configuration.pathAccessible(fullPath, mode: X_OK) if fileExists { return fullPath } } + throw SubprocessError( + code: .init(.executableNotFound(executableName)), + underlyingError: nil + ) case .path(let executablePath): // Use path directly return executablePath.string } - return nil } } -// MARK: - Configuration -extension Subprocess.Configuration { - internal func preSpawn() throws -> ( - executablePath: String, +// MARK: - PreSpawn +extension Configuration { + internal typealias PreSpawnArgs = ( env: [UnsafeMutablePointer?], - argv: [UnsafeMutablePointer?], - intendedWorkingDir: FilePath, uidPtr: UnsafeMutablePointer?, gidPtr: UnsafeMutablePointer?, supplementaryGroups: [gid_t]? - ) { + ) + + internal func preSpawn( + _ work: (PreSpawnArgs) throws -> Result + ) throws -> Result { // Prepare environment let env = self.environment.createEnv() - // Prepare executable path - guard let executablePath = self.executable.resolveExecutablePath( - withPathValue: self.environment.pathValue()) else { - for ptr in env { ptr?.deallocate() } - throw CocoaError(.executableNotLoadable, userInfo: [ - .debugDescriptionErrorKey : "\(self.executable.description) is not an executable" - ]) - } - // Prepare arguments - let argv: [UnsafeMutablePointer?] = self.arguments.createArgs(withExecutablePath: executablePath) - // Prepare workingDir - let intendedWorkingDir = self.workingDirectory - guard Self.pathAccessible(intendedWorkingDir.string, mode: F_OK) else { + defer { for ptr in env { ptr?.deallocate() } - for ptr in argv { ptr?.deallocate() } - throw CocoaError(.fileNoSuchFile, userInfo: [ - .debugDescriptionErrorKey : "Failed to set working directory to \(intendedWorkingDir)" - ]) } var uidPtr: UnsafeMutablePointer? = nil @@ -325,21 +339,28 @@ extension Subprocess.Configuration { uidPtr = .allocate(capacity: 1) uidPtr?.pointee = userID } + defer { + uidPtr?.deallocate() + } var gidPtr: UnsafeMutablePointer? = nil if let groupID = self.platformOptions.groupID { gidPtr = .allocate(capacity: 1) gidPtr?.pointee = groupID } + defer { + gidPtr?.deallocate() + } var supplementaryGroups: [gid_t]? if let groupsValue = self.platformOptions.supplementaryGroups { supplementaryGroups = groupsValue } - return ( - executablePath: executablePath, - env: env, argv: argv, - intendedWorkingDir: intendedWorkingDir, - uidPtr: uidPtr, gidPtr: gidPtr, - supplementaryGroups: supplementaryGroups + return try work( + ( + env: env, + uidPtr: uidPtr, + gidPtr: gidPtr, + supplementaryGroups: supplementaryGroups + ) ) } @@ -359,11 +380,11 @@ extension FileDescriptor { return devnull } - internal var platformDescriptor: Subprocess.PlatformFileDescriptor { + internal var platformDescriptor: PlatformFileDescriptor { return self } - internal func readChunk(upToLength maxLength: Int) async throws -> Data? { + package func readChunk(upToLength maxLength: Int) async throws -> SequenceOutput.Buffer? { return try await withCheckedThrowingContinuation { continuation in DispatchIO.read( fromFileDescriptor: self.rawValue, @@ -371,88 +392,125 @@ extension FileDescriptor { runningHandlerOn: .global() ) { data, error in if error != 0 { - continuation.resume(throwing: POSIXError(.init(rawValue: error) ?? .ENODEV)) + continuation.resume( + throwing: SubprocessError( + code: .init(.failedToReadFromSubprocess), + underlyingError: .init(rawValue: error) + ) + ) return } if data.isEmpty { continuation.resume(returning: nil) } else { - continuation.resume(returning: Data(data)) + continuation.resume(returning: SequenceOutput.Buffer(data: data)) } } } } - internal func readUntilEOF(upToLength maxLength: Int) async throws -> Data { - return try await withCheckedThrowingContinuation { continuation in - let dispatchIO = DispatchIO( - type: .stream, - fileDescriptor: self.rawValue, - queue: .global() - ) { error in - if error != 0 { - continuation.resume(throwing: POSIXError(.init(rawValue: error) ?? .ENODEV)) - } + internal func readUntilEOF( + upToLength maxLength: Int, + resultHandler: sending @escaping (Swift.Result) -> Void + ) { + let dispatchIO = DispatchIO( + type: .stream, + fileDescriptor: self.rawValue, + queue: .global() + ) { error in } + var buffer: DispatchData? + dispatchIO.read( + offset: 0, + length: maxLength, + queue: .global() + ) { done, data, error in + guard error == 0, let chunkData = data else { + dispatchIO.close() + resultHandler( + .failure( + SubprocessError( + code: .init(.failedToReadFromSubprocess), + underlyingError: .init(rawValue: error) + ) + ) + ) + return } - var buffer: Data = Data() - dispatchIO.read( - offset: 0, - length: maxLength, - queue: .global() - ) { done, data, error in - guard error == 0 else { - continuation.resume(throwing: POSIXError(.init(rawValue: error) ?? .ENODEV)) - return - } - if let data = data { - buffer += Data(data) - } - if done { - dispatchIO.close() - continuation.resume(returning: buffer) - } + // Easy case: if we are done and buffer is nil, this means + // there is only one chunk of data + if done && buffer == nil { + dispatchIO.close() + buffer = chunkData + resultHandler(.success(chunkData)) + return + } + + if buffer == nil { + buffer = chunkData + } else { + buffer?.append(chunkData) + } + + if done { + dispatchIO.close() + resultHandler(.success(buffer!)) + return } } } - internal func write(_ data: S) async throws where S.Element == UInt8 { - try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) -> Void in - let dispatchData: DispatchData = Array(data).withUnsafeBytes { - return DispatchData(bytes: $0) - } - DispatchIO.write( - toFileDescriptor: self.rawValue, - data: dispatchData, - runningHandlerOn: .global() - ) { _, error in - guard error == 0 else { - continuation.resume( - throwing: POSIXError( - .init(rawValue: error) ?? .ENODEV) + package func write( + _ array: [UInt8] + ) async throws -> Int { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + let dispatchData = array.withUnsafeBytes { + return DispatchData( + bytesNoCopy: $0, + deallocator: .custom( + nil, + { + // noop + } ) - return + ) + } + self.write(dispatchData) { writtenLength, error in + if let error = error { + continuation.resume(throwing: error) + } else { + continuation.resume(returning: writtenLength) } - continuation.resume() } } } -} -extension Subprocess { - internal typealias PlatformFileDescriptor = FileDescriptor -} - -// MARK: - Read Buffer Size -extension Subprocess { - @inline(__always) - internal static var readBufferSize: Int { -#if canImport(Darwin) - return 16384 -#else - // FIXME: Use Platform.pageSize here - return 4096 -#endif // canImport(Darwin) + package func write( + _ dispatchData: DispatchData, + queue: DispatchQueue = .global(), + completion: @escaping (Int, Error?) -> Void + ) { + DispatchIO.write( + toFileDescriptor: self.rawValue, + data: dispatchData, + runningHandlerOn: queue + ) { unwritten, error in + let unwrittenLength = unwritten?.count ?? 0 + let writtenLength = dispatchData.count - unwrittenLength + guard error != 0 else { + completion(writtenLength, nil) + return + } + completion( + writtenLength, + SubprocessError( + code: .init(.failedToWriteToSubprocess), + underlyingError: .init(rawValue: error) + ) + ) + } } } -#endif // canImport(Darwin) || canImport(Glibc) +internal typealias PlatformFileDescriptor = FileDescriptor + +#endif // canImport(Darwin) || canImport(Glibc) || canImport(Bionic) || canImport(Musl) diff --git a/Sources/_Subprocess/Platforms/Subprocess+Windows.swift b/Sources/_Subprocess/Platforms/Subprocess+Windows.swift index 978cb139e..e84d14148 100644 --- a/Sources/_Subprocess/Platforms/Subprocess+Windows.swift +++ b/Sources/_Subprocess/Platforms/Subprocess+Windows.swift @@ -2,53 +2,66 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2024 Apple Inc. and the Swift.org project authors -// Licensed under Apache License v2.0 +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception // -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift.org project authors -// -// SPDX-License-Identifier: Apache-2.0 +// See https://swift.org/LICENSE.txt for license information // //===----------------------------------------------------------------------===// #if canImport(WinSDK) import WinSDK -import Dispatch -import SystemPackage -import FoundationEssentials +internal import Dispatch +#if canImport(System) +import System +#else +@preconcurrency import SystemPackage +#endif // Windows specific implementation -extension Subprocess.Configuration { - internal func spawn( - withInput input: Subprocess.ExecutionInput, - output: Subprocess.ExecutionOutput, - error: Subprocess.ExecutionOutput - ) throws -> Subprocess { +extension Configuration { + internal func spawn< + Output: OutputProtocol, + Error: OutputProtocol + >( + withInput inputPipe: CreatedPipe, + output: Output, + outputPipe: CreatedPipe, + error: Error, + errorPipe: CreatedPipe + ) throws -> Execution { // Spawn differently depending on whether // we need to spawn as a user - if let userCredentials = self.platformOptions.userCredentials { - return try self.spawnAsUser( - withInput: input, - output: output, - error: error, - userCredentials: userCredentials - ) - } else { + guard let userCredentials = self.platformOptions.userCredentials else { return try self.spawnDirect( - withInput: input, + withInput: inputPipe, output: output, - error: error + outputPipe: outputPipe, + error: error, + errorPipe: errorPipe ) } + return try self.spawnAsUser( + withInput: inputPipe, + output: output, + outputPipe: outputPipe, + error: error, + errorPipe: errorPipe, + userCredentials: userCredentials + ) } - internal func spawnDirect( - withInput input: Subprocess.ExecutionInput, - output: Subprocess.ExecutionOutput, - error: Subprocess.ExecutionOutput - ) throws -> Subprocess { + internal func spawnDirect< + Output: OutputProtocol, + Error: OutputProtocol + >( + withInput inputPipe: CreatedPipe, + output: Output, + outputPipe: CreatedPipe, + error: Error, + errorPipe: CreatedPipe + ) throws -> Execution { let ( applicationName, commandAndArgs, @@ -56,9 +69,9 @@ extension Subprocess.Configuration { intendedWorkingDir ) = try self.preSpawn() var startupInfo = try self.generateStartupInfo( - withInput: input, - output: output, - error: error + withInput: inputPipe, + output: outputPipe, + error: errorPipe ) var processInfo: PROCESS_INFORMATION = PROCESS_INFORMATION() var createProcessFlags = self.generateCreateProcessFlag() @@ -78,8 +91,8 @@ extension Subprocess.Configuration { let created = CreateProcessW( applicationNameW, UnsafeMutablePointer(mutating: commandAndArgsW), - nil, // lpProcessAttributes - nil, // lpThreadAttributes + nil, // lpProcessAttributes + nil, // lpThreadAttributes true, // bInheritHandles createProcessFlags, UnsafeMutableRawPointer(mutating: environmentW), @@ -89,14 +102,14 @@ extension Subprocess.Configuration { ) guard created else { let windowsError = GetLastError() - try self.cleanupAll( - input: input, - output: output, - error: error + try self.cleanupPreSpawn( + input: inputPipe, + output: outputPipe, + error: errorPipe ) - throw CocoaError.windowsError( - underlying: windowsError, - errorCode: .fileWriteUnknown + throw SubprocessError( + code: .init(.spawnFailed), + underlyingError: .init(rawValue: windowsError) ) } } @@ -106,47 +119,52 @@ extension Subprocess.Configuration { // We don't need the handle objects, so close it right away guard CloseHandle(processInfo.hThread) else { let windowsError = GetLastError() - try self.cleanupAll( - input: input, - output: output, - error: error + try self.cleanupPreSpawn( + input: inputPipe, + output: outputPipe, + error: errorPipe ) - throw CocoaError.windowsError( - underlying: windowsError, - errorCode: .fileReadUnknown + throw SubprocessError( + code: .init(.spawnFailed), + underlyingError: .init(rawValue: windowsError) ) } guard CloseHandle(processInfo.hProcess) else { let windowsError = GetLastError() - try self.cleanupAll( - input: input, - output: output, - error: error + try self.cleanupPreSpawn( + input: inputPipe, + output: outputPipe, + error: errorPipe ) - throw CocoaError.windowsError( - underlying: windowsError, - errorCode: .fileReadUnknown + throw SubprocessError( + code: .init(.spawnFailed), + underlyingError: .init(rawValue: windowsError) ) } - let pid = Subprocess.ProcessIdentifier( - processID: processInfo.dwProcessId, - threadID: processInfo.dwThreadId + let pid = ProcessIdentifier( + value: processInfo.dwProcessId ) - return Subprocess( + return Execution( processIdentifier: pid, - executionInput: input, - executionOutput: output, - executionError: error, + output: output, + error: error, + outputPipe: outputPipe, + errorPipe: errorPipe, consoleBehavior: self.platformOptions.consoleBehavior ) } - internal func spawnAsUser( - withInput input: Subprocess.ExecutionInput, - output: Subprocess.ExecutionOutput, - error: Subprocess.ExecutionOutput, - userCredentials: Subprocess.PlatformOptions.UserCredentials - ) throws -> Subprocess { + internal func spawnAsUser< + Output: OutputProtocol, + Error: OutputProtocol + >( + withInput inputPipe: CreatedPipe, + output: Output, + outputPipe: CreatedPipe, + error: Error, + errorPipe: CreatedPipe, + userCredentials: PlatformOptions.UserCredentials + ) throws -> Execution { let ( applicationName, commandAndArgs, @@ -154,9 +172,9 @@ extension Subprocess.Configuration { intendedWorkingDir ) = try self.preSpawn() var startupInfo = try self.generateStartupInfo( - withInput: input, - output: output, - error: error + withInput: inputPipe, + output: outputPipe, + error: errorPipe ) var processInfo: PROCESS_INFORMATION = PROCESS_INFORMATION() var createProcessFlags = self.generateCreateProcessFlag() @@ -197,14 +215,14 @@ extension Subprocess.Configuration { ) guard created else { let windowsError = GetLastError() - try self.cleanupAll( - input: input, - output: output, - error: error + try self.cleanupPreSpawn( + input: inputPipe, + output: outputPipe, + error: errorPipe ) - throw CocoaError.windowsError( - underlying: windowsError, - errorCode: .fileWriteUnknown + throw SubprocessError( + code: .init(.spawnFailed), + underlyingError: .init(rawValue: windowsError) ) } } @@ -217,201 +235,175 @@ extension Subprocess.Configuration { // We don't need the handle objects, so close it right away guard CloseHandle(processInfo.hThread) else { let windowsError = GetLastError() - try self.cleanupAll( - input: input, - output: output, - error: error + try self.cleanupPreSpawn( + input: inputPipe, + output: outputPipe, + error: errorPipe ) - throw CocoaError.windowsError( - underlying: windowsError, - errorCode: .fileReadUnknown + throw SubprocessError( + code: .init(.spawnFailed), + underlyingError: .init(rawValue: windowsError) ) } guard CloseHandle(processInfo.hProcess) else { let windowsError = GetLastError() - try self.cleanupAll( - input: input, - output: output, - error: error + try self.cleanupPreSpawn( + input: inputPipe, + output: outputPipe, + error: errorPipe ) - throw CocoaError.windowsError( - underlying: windowsError, - errorCode: .fileReadUnknown + throw SubprocessError( + code: .init(.spawnFailed), + underlyingError: .init(rawValue: windowsError) ) } - let pid = Subprocess.ProcessIdentifier( - processID: processInfo.dwProcessId, - threadID: processInfo.dwThreadId + let pid = ProcessIdentifier( + value: processInfo.dwProcessId ) - return Subprocess( + return Execution( processIdentifier: pid, - executionInput: input, - executionOutput: output, - executionError: error, + output: output, + error: error, + outputPipe: outputPipe, + errorPipe: errorPipe, consoleBehavior: self.platformOptions.consoleBehavior ) } } // MARK: - Platform Specific Options -extension Subprocess { - /// The collection of platform-specific settings - /// to configure the subprocess when running - public struct PlatformOptions: Sendable { - /// A `UserCredentials` to use spawning the subprocess - /// as a different user - public struct UserCredentials: Sendable, Hashable { - // The name of the user. This is the name - // of the user account to run as. - public var username: String - // The clear-text password for the account. - public var password: String - // The name of the domain or server whose account database - // contains the account. - public var domain: String? - } - /// `ConsoleBehavior` defines how should the console appear - /// when spawning a new process - public struct ConsoleBehavior: Sendable, Hashable { - internal enum Storage: Sendable, Hashable { - case createNew - case detatch - case inherit - } +/// The collection of platform-specific settings +/// to configure the subprocess when running +public struct PlatformOptions: Sendable { + /// A `UserCredentials` to use spawning the subprocess + /// as a different user + public struct UserCredentials: Sendable, Hashable { + // The name of the user. This is the name + // of the user account to run as. + public var username: String + // The clear-text password for the account. + public var password: String + // The name of the domain or server whose account database + // contains the account. + public var domain: String? + } - internal let storage: Storage + /// `ConsoleBehavior` defines how should the console appear + /// when spawning a new process + public struct ConsoleBehavior: Sendable, Hashable { + internal enum Storage: Sendable, Hashable { + case createNew + case detatch + case inherit + } - private init(_ storage: Storage) { - self.storage = storage - } + internal let storage: Storage - /// The subprocess has a new console, instead of - /// inheriting its parent's console (the default). - public static let createNew: Self = .init(.createNew) - /// For console processes, the new process does not - /// inherit its parent's console (the default). - /// The new process can call the `AllocConsole` - /// function at a later time to create a console. - public static let detatch: Self = .init(.detatch) - /// The subprocess inherits its parent's console. - public static let inherit: Self = .init(.inherit) + private init(_ storage: Storage) { + self.storage = storage } - /// `ConsoleBehavior` defines how should the window appear - /// when spawning a new process - public struct WindowStyle: Sendable, Hashable { - internal enum Storage: Sendable, Hashable { - case normal - case hidden - case maximized - case minimized - } + /// The subprocess has a new console, instead of + /// inheriting its parent's console (the default). + public static let createNew: Self = .init(.createNew) + /// For console processes, the new process does not + /// inherit its parent's console (the default). + /// The new process can call the `AllocConsole` + /// function at a later time to create a console. + public static let detatch: Self = .init(.detatch) + /// The subprocess inherits its parent's console. + public static let inherit: Self = .init(.inherit) + } - internal let storage: Storage + /// `ConsoleBehavior` defines how should the window appear + /// when spawning a new process + public struct WindowStyle: Sendable, Hashable { + internal enum Storage: Sendable, Hashable { + case normal + case hidden + case maximized + case minimized + } - internal var platformStyle: WORD { - switch self.storage { - case .hidden: return WORD(SW_HIDE) - case .maximized: return WORD(SW_SHOWMAXIMIZED) - case .minimized: return WORD(SW_SHOWMINIMIZED) - default: return WORD(SW_SHOWNORMAL) - } - } + internal let storage: Storage - private init(_ storage: Storage) { - self.storage = storage + internal var platformStyle: WORD { + switch self.storage { + case .hidden: return WORD(SW_HIDE) + case .maximized: return WORD(SW_SHOWMAXIMIZED) + case .minimized: return WORD(SW_SHOWMINIMIZED) + default: return WORD(SW_SHOWNORMAL) } + } - /// Activates and displays a window of normal size - public static let normal: Self = .init(.normal) - /// Does not activate a new window - public static let hidden: Self = .init(.hidden) - /// Activates the window and displays it as a maximized window. - public static let maximized: Self = .init(.maximized) - /// Activates the window and displays it as a minimized window. - public static let minimized: Self = .init(.minimized) + private init(_ storage: Storage) { + self.storage = storage } - /// Sets user credentials when starting the process as another user - public var userCredentials: UserCredentials? = nil - /// The console behavior of the new process, - /// default to inheriting the console from parent process - public var consoleBehavior: ConsoleBehavior = .inherit - /// Window style to use when the process is started - public var windowStyle: WindowStyle = .normal - /// Whether to create a new process group for the new - /// process. The process group includes all processes - /// that are descendants of this root process. - /// The process identifier of the new process group - /// is the same as the process identifier. - public var createProcessGroup: Bool = false - /// A closure to configure platform-specific - /// spawning constructs. This closure enables direct - /// configuration or override of underlying platform-specific - /// spawn settings that `Subprocess` utilizes internally, - /// in cases where Subprocess does not provide higher-level - /// APIs for such modifications. - /// - /// On Windows, Subprocess uses `CreateProcessW()` as the - /// underlying spawning mechanism. This closure allows - /// modification of the `dwCreationFlags` creation flag - /// and startup info `STARTUPINFOW` before - /// they are sent to `CreateProcessW()`. - public var preSpawnProcessConfigurator: ( + /// Activates and displays a window of normal size + public static let normal: Self = .init(.normal) + /// Does not activate a new window + public static let hidden: Self = .init(.hidden) + /// Activates the window and displays it as a maximized window. + public static let maximized: Self = .init(.maximized) + /// Activates the window and displays it as a minimized window. + public static let minimized: Self = .init(.minimized) + } + + /// Sets user credentials when starting the process as another user + public var userCredentials: UserCredentials? = nil + /// The console behavior of the new process, + /// default to inheriting the console from parent process + public var consoleBehavior: ConsoleBehavior = .inherit + /// Window style to use when the process is started + public var windowStyle: WindowStyle = .normal + /// Whether to create a new process group for the new + /// process. The process group includes all processes + /// that are descendants of this root process. + /// The process identifier of the new process group + /// is the same as the process identifier. + public var createProcessGroup: Bool = false + /// An ordered list of steps in order to tear down the child + /// process in case the parent task is cancelled before + /// the child proces terminates. + /// Always ends in forcefully terminate at the end. + public var teardownSequence: [TeardownStep] = [] + /// A closure to configure platform-specific + /// spawning constructs. This closure enables direct + /// configuration or override of underlying platform-specific + /// spawn settings that `Subprocess` utilizes internally, + /// in cases where Subprocess does not provide higher-level + /// APIs for such modifications. + /// + /// On Windows, Subprocess uses `CreateProcessW()` as the + /// underlying spawning mechanism. This closure allows + /// modification of the `dwCreationFlags` creation flag + /// and startup info `STARTUPINFOW` before + /// they are sent to `CreateProcessW()`. + public var preSpawnProcessConfigurator: + ( @Sendable ( inout DWORD, inout STARTUPINFOW ) throws -> Void )? = nil - public init() {} - } -} - -extension Subprocess.PlatformOptions: Hashable { - public static func == ( - lhs: Subprocess.PlatformOptions, - rhs: Subprocess.PlatformOptions - ) -> Bool { - // Since we can't compare closure equality, - // as long as preSpawnProcessConfigurator is set - // always returns false so that `PlatformOptions` - // with it set will never equal to each other - if lhs.preSpawnProcessConfigurator != nil || rhs.preSpawnProcessConfigurator != nil { - return false - } - return lhs.userCredentials == rhs.userCredentials && lhs.consoleBehavior == rhs.consoleBehavior && lhs.windowStyle == rhs.windowStyle && - lhs.createProcessGroup == rhs.createProcessGroup - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(userCredentials) - hasher.combine(consoleBehavior) - hasher.combine(windowStyle) - hasher.combine(createProcessGroup) - // Since we can't really hash closures, - // use an UUID such that as long as - // `preSpawnProcessConfigurator` is set, it will - // never equal to other PlatformOptions - if self.preSpawnProcessConfigurator != nil { - hasher.combine(UUID()) - } - } + public init() {} } -extension Subprocess.PlatformOptions : CustomStringConvertible, CustomDebugStringConvertible { +extension PlatformOptions: CustomStringConvertible, CustomDebugStringConvertible { internal func description(withIndent indent: Int) -> String { let indent = String(repeating: " ", count: indent * 4) return """ -PlatformOptions( -\(indent) userCredentials: \(String(describing: self.userCredentials)), -\(indent) consoleBehavior: \(String(describing: self.consoleBehavior)), -\(indent) windowStyle: \(String(describing: self.windowStyle)), -\(indent) createProcessGroup: \(self.createProcessGroup), -\(indent) preSpawnProcessConfigurator: \(self.preSpawnProcessConfigurator == nil ? "not set" : "set") -\(indent)) -""" + PlatformOptions( + \(indent) userCredentials: \(String(describing: self.userCredentials)), + \(indent) consoleBehavior: \(String(describing: self.consoleBehavior)), + \(indent) windowStyle: \(String(describing: self.windowStyle)), + \(indent) createProcessGroup: \(self.createProcessGroup), + \(indent) preSpawnProcessConfigurator: \(self.preSpawnProcessConfigurator == nil ? "not set" : "set") + \(indent)) + """ } public var description: String { @@ -426,8 +418,8 @@ PlatformOptions( // MARK: - Process Monitoring @Sendable internal func monitorProcessTermination( - forProcessWithIdentifier pid: Subprocess.ProcessIdentifier -) async throws -> Subprocess.TerminationStatus { + forProcessWithIdentifier pid: ProcessIdentifier +) async throws -> TerminationStatus { // Once the continuation resumes, it will need to unregister the wait, so // yield the wait handle back to the calling scope. var waitHandle: HANDLE? @@ -436,11 +428,13 @@ internal func monitorProcessTermination( _ = UnregisterWait(waitHandle) } } - guard let processHandle = OpenProcess( - DWORD(PROCESS_QUERY_INFORMATION | SYNCHRONIZE), - false, - pid.processID - ) else { + guard + let processHandle = OpenProcess( + DWORD(PROCESS_QUERY_INFORMATION | SYNCHRONIZE), + false, + pid.value + ) + else { return .exited(1) } @@ -449,19 +443,29 @@ internal func monitorProcessTermination( // other work. let context = Unmanaged.passRetained(continuation as AnyObject).toOpaque() let callback: WAITORTIMERCALLBACK = { context, _ in - let continuation = Unmanaged.fromOpaque(context!).takeRetainedValue() as! CheckedContinuation + let continuation = + Unmanaged.fromOpaque(context!).takeRetainedValue() as! CheckedContinuation continuation.resume() } // We only want the callback to fire once (and not be rescheduled.) Waiting // may take an arbitrarily long time, so let the thread pool know that too. let flags = ULONG(WT_EXECUTEONLYONCE | WT_EXECUTELONGFUNCTION) - guard RegisterWaitForSingleObject( - &waitHandle, processHandle, callback, context, INFINITE, flags - ) else { - continuation.resume(throwing: CocoaError.windowsError( - underlying: GetLastError(), - errorCode: .fileWriteUnknown) + guard + RegisterWaitForSingleObject( + &waitHandle, + processHandle, + callback, + context, + INFINITE, + flags + ) + else { + continuation.resume( + throwing: SubprocessError( + code: .init(.failedToMonitorProcess), + underlyingError: .init(rawValue: GetLastError()) + ) ) return } @@ -474,58 +478,62 @@ internal func monitorProcessTermination( return .exited(1) } let exitCodeValue = CInt(bitPattern: .init(status)) - if exitCodeValue >= 0 { - return .exited(status) - } else { + guard exitCodeValue >= 0 else { return .unhandledException(status) } + return .exited(status) } // MARK: - Subprocess Control -extension Subprocess { +@available(macOS 15.0, *) // FIXME: manually added availability +extension Execution { /// Terminate the current subprocess with the given exit code /// - Parameter exitCode: The exit code to use for the subprocess. public func terminate(withExitCode exitCode: DWORD) throws { - guard let processHandle = OpenProcess( - // PROCESS_ALL_ACCESS - DWORD(STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0xFFFF), - false, - self.processIdentifier.processID - ) else { - throw CocoaError.windowsError( - underlying: GetLastError(), - errorCode: .fileWriteUnknown + guard + let processHandle = OpenProcess( + // PROCESS_ALL_ACCESS + DWORD(STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0xFFFF), + false, + self.processIdentifier.value + ) + else { + throw SubprocessError( + code: .init(.failedToTerminate), + underlyingError: .init(rawValue: GetLastError()) ) } defer { CloseHandle(processHandle) } guard TerminateProcess(processHandle, exitCode) else { - throw CocoaError.windowsError( - underlying: GetLastError(), - errorCode: .fileWriteUnknown + throw SubprocessError( + code: .init(.failedToTerminate), + underlyingError: .init(rawValue: GetLastError()) ) } } /// Suspend the current subprocess public func suspend() throws { - guard let processHandle = OpenProcess( - // PROCESS_ALL_ACCESS - DWORD(STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0xFFFF), - false, - self.processIdentifier.processID - ) else { - throw CocoaError.windowsError( - underlying: GetLastError(), - errorCode: .fileWriteUnknown + guard + let processHandle = OpenProcess( + // PROCESS_ALL_ACCESS + DWORD(STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0xFFFF), + false, + self.processIdentifier.value + ) + else { + throw SubprocessError( + code: .init(.failedToSuspend), + underlyingError: .init(rawValue: GetLastError()) ) } defer { CloseHandle(processHandle) } - let NTSuspendProcess: Optional<(@convention(c) (HANDLE) -> LONG)> = + let NTSuspendProcess: (@convention(c) (HANDLE) -> LONG)? = unsafeBitCast( GetProcAddress( GetModuleHandleA("ntdll.dll"), @@ -534,53 +542,61 @@ extension Subprocess { to: Optional<(@convention(c) (HANDLE) -> LONG)>.self ) guard let NTSuspendProcess = NTSuspendProcess else { - throw CocoaError(.executableNotLoadable) + throw SubprocessError( + code: .init(.failedToSuspend), + underlyingError: .init(rawValue: GetLastError()) + ) } guard NTSuspendProcess(processHandle) >= 0 else { - throw CocoaError.windowsError( - underlying: GetLastError(), - errorCode: .fileWriteUnknown + throw SubprocessError( + code: .init(.failedToSuspend), + underlyingError: .init(rawValue: GetLastError()) ) } } /// Resume the current subprocess after suspension public func resume() throws { - guard let processHandle = OpenProcess( - // PROCESS_ALL_ACCESS - DWORD(STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0xFFFF), - false, - self.processIdentifier.processID - ) else { - throw CocoaError.windowsError( - underlying: GetLastError(), - errorCode: .fileWriteUnknown + guard + let processHandle = OpenProcess( + // PROCESS_ALL_ACCESS + DWORD(STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0xFFFF), + false, + self.processIdentifier.value + ) + else { + throw SubprocessError( + code: .init(.failedToResume), + underlyingError: .init(rawValue: GetLastError()) ) } defer { CloseHandle(processHandle) } - let NTResumeProcess: Optional<(@convention(c) (HANDLE) -> LONG)> = - unsafeBitCast( - GetProcAddress( - GetModuleHandleA("ntdll.dll"), - "NtResumeProcess" - ), - to: Optional<(@convention(c) (HANDLE) -> LONG)>.self - ) + let NTResumeProcess: (@convention(c) (HANDLE) -> LONG)? = + unsafeBitCast( + GetProcAddress( + GetModuleHandleA("ntdll.dll"), + "NtResumeProcess" + ), + to: Optional<(@convention(c) (HANDLE) -> LONG)>.self + ) guard let NTResumeProcess = NTResumeProcess else { - throw CocoaError(.executableNotLoadable) + throw SubprocessError( + code: .init(.failedToResume), + underlyingError: .init(rawValue: GetLastError()) + ) } guard NTResumeProcess(processHandle) >= 0 else { - throw CocoaError.windowsError( - underlying: GetLastError(), - errorCode: .fileWriteUnknown + throw SubprocessError( + code: .init(.failedToResume), + underlyingError: .init(rawValue: GetLastError()) ) } } - internal func tryTerminate() -> Error? { + internal func tryTerminate() -> Swift.Error? { do { try self.terminate(withExitCode: 0) } catch { @@ -591,35 +607,44 @@ extension Subprocess { } // MARK: - Executable Searching -extension Subprocess.Executable { +extension Executable { // Technically not needed for CreateProcess since // it takes process name. It's here to support // Executable.resolveExecutablePath - internal func resolveExecutablePath(withPathValue pathValue: String?) -> String? { + internal func resolveExecutablePath(withPathValue pathValue: String?) throws -> String { switch self.storage { case .executable(let executableName): - return executableName.withCString( + return try executableName.withCString( encodedAs: UTF16.self - ) { exeName -> String? in - return pathValue.withOptionalCString( + ) { exeName -> String in + return try pathValue.withOptionalCString( encodedAs: UTF16.self - ) { path -> String? in + ) { path -> String in let pathLenth = SearchPathW( path, exeName, - nil, 0, nil, nil + nil, + 0, + nil, + nil ) guard pathLenth > 0 else { - return nil + throw SubprocessError( + code: .init(.executableNotFound(executableName)), + underlyingError: .init(rawValue: GetLastError()) + ) } return withUnsafeTemporaryAllocation( - of: WCHAR.self, capacity: Int(pathLenth) + 1 + of: WCHAR.self, + capacity: Int(pathLenth) + 1 ) { _ = SearchPathW( path, - exeName, nil, + exeName, + nil, pathLenth + 1, - $0.baseAddress, nil + $0.baseAddress, + nil ) return String(decodingCString: $0.baseAddress!, as: UTF16.self) } @@ -633,49 +658,60 @@ extension Subprocess.Executable { } // MARK: - Environment Resolution -extension Subprocess.Environment { - internal static let pathEnvironmentVariableName = "Path" +extension Environment { + internal static let pathVariableName = "Path" internal func pathValue() -> String? { switch self.config { case .inherit(let overrides): // If PATH value exists in overrides, use it - if let value = overrides[.string(Self.pathEnvironmentVariableName)] { - return value.stringValue + if let value = overrides[Self.pathVariableName] { + return value } // Fall back to current process - return ProcessInfo.processInfo.environment[Self.pathEnvironmentVariableName] + return Self.currentEnvironmentValues()[Self.pathVariableName] case .custom(let fullEnvironment): - if let value = fullEnvironment[.string(Self.pathEnvironmentVariableName)] { - return value.stringValue + if let value = fullEnvironment[Self.pathVariableName] { + return value } return nil } } + + internal static func withCopiedEnv(_ body: ([UnsafeMutablePointer]) -> R) -> R { + var values: [UnsafeMutablePointer] = [] + guard let pwszEnvironmentBlock = GetEnvironmentStringsW() else { + return body([]) + } + defer { FreeEnvironmentStringsW(pwszEnvironmentBlock) } + + var pwszEnvironmentEntry: LPWCH? = pwszEnvironmentBlock + while let value = pwszEnvironmentEntry { + let entry = String(decodingCString: value, as: UTF16.self) + if entry.isEmpty { break } + values.append(entry.withCString { _strdup($0)! }) + pwszEnvironmentEntry = pwszEnvironmentEntry?.advanced(by: wcslen(value) + 1) + } + defer { values.forEach { free($0) } } + return body(values) + } } // MARK: - ProcessIdentifier -extension Subprocess { - /// A platform independent identifier for a subprocess. - public struct ProcessIdentifier: Sendable, Hashable, Codable { - /// Windows specifc process identifier value - public let processID: DWORD - /// Windows specific thread identifier associated with process - public let threadID: DWORD - - internal init( - processID: DWORD, - threadID: DWORD - ) { - self.processID = processID - self.threadID = threadID - } + +/// A platform independent identifier for a subprocess. +public struct ProcessIdentifier: Sendable, Hashable, Codable { + /// Windows specifc process identifier value + public let value: DWORD + + internal init(value: DWORD) { + self.value = value } } -extension Subprocess.ProcessIdentifier: CustomStringConvertible, CustomDebugStringConvertible { +extension ProcessIdentifier: CustomStringConvertible, CustomDebugStringConvertible { public var description: String { - return "(processID: \(self.processID), threadID: \(self.threadID))" + return "(processID: \(self.value))" } public var debugDescription: String { @@ -684,7 +720,7 @@ extension Subprocess.ProcessIdentifier: CustomStringConvertible, CustomDebugStri } // MARK: - Private Utils -extension Subprocess.Configuration { +extension Configuration { private func preSpawn() throws -> ( applicationName: String?, commandAndArgs: String, @@ -692,44 +728,33 @@ extension Subprocess.Configuration { intendedWorkingDir: String ) { // Prepare environment - var env: [String : String] = [:] + var env: [String: String] = [:] switch self.environment.config { case .custom(let customValues): // Use the custom values directly - for customKey in customValues.keys { - guard case .string(let stringKey) = customKey, - let valueContainer = customValues[customKey], - case .string(let stringValue) = valueContainer else { - fatalError("Windows does not support non unicode String as environments") - } - env.updateValue(stringValue, forKey: stringKey) - } + env = customValues case .inherit(let updateValues): // Combine current environment - env = ProcessInfo.processInfo.environment - for updatingKey in updateValues.keys { - // Override the current environment values - guard case .string(let stringKey) = updatingKey, - let valueContainer = updateValues[updatingKey], - case .string(let stringValue) = valueContainer else { - fatalError("Windows does not support non unicode String as environments") - } - env.updateValue(stringValue, forKey: stringKey) + env = Environment.currentEnvironmentValues() + for (key, value) in updateValues { + env.updateValue(value, forKey: key) } } // On Windows, the PATH is required in order to locate dlls needed by // the process so we should also pass that to the child - let pathVariableName = Subprocess.Environment.pathEnvironmentVariableName + let pathVariableName = Environment.pathVariableName if env[pathVariableName] == nil, - let parentPath = ProcessInfo.processInfo.environment[pathVariableName] { + let parentPath = Environment.currentEnvironmentValues()[pathVariableName] + { env[pathVariableName] = parentPath } // The environment string must be terminated by a double // null-terminator. Otherwise, CreateProcess will fail with // INVALID_PARMETER. - let environmentString = env.map { - $0.key + "=" + $0.value - }.joined(separator: "\0") + "\0\0" + let environmentString = + env.map { + $0.key + "=" + $0.value + }.joined(separator: "\0") + "\0\0" // Prepare arguments let ( @@ -738,9 +763,12 @@ extension Subprocess.Configuration { ) = try self.generateWindowsCommandAndAgruments() // Validate workingDir guard Self.pathAccessible(self.workingDirectory.string) else { - throw CocoaError(.fileNoSuchFile, userInfo: [ - .debugDescriptionErrorKey : "Failed to set working directory to \(self.workingDirectory)" - ]) + throw SubprocessError( + code: .init( + .failedToChangeWorkingDirectory(self.workingDirectory.string) + ), + underlyingError: nil + ) } return ( applicationName: applicationName, @@ -767,9 +795,9 @@ extension Subprocess.Configuration { } private func generateStartupInfo( - withInput input: Subprocess.ExecutionInput, - output: Subprocess.ExecutionOutput, - error: Subprocess.ExecutionOutput + withInput input: CreatedPipe, + output: CreatedPipe, + error: CreatedPipe ) throws -> STARTUPINFOW { var info: STARTUPINFOW = STARTUPINFOW() info.cb = DWORD(MemoryLayout.size) @@ -781,10 +809,10 @@ extension Subprocess.Configuration { } // Bind IOs // Input - if let inputRead = input.getReadFileDescriptor() { + if let inputRead = input.readFileDescriptor { info.hStdInput = inputRead.platformDescriptor } - if let inputWrite = input.getWriteFileDescriptor() { + if let inputWrite = input.writeFileDescriptor { // Set parent side to be uninhertable SetHandleInformation( inputWrite.platformDescriptor, @@ -793,10 +821,10 @@ extension Subprocess.Configuration { ) } // Output - if let outputWrite = output.getWriteFileDescriptor() { + if let outputWrite = output.writeFileDescriptor { info.hStdOutput = outputWrite.platformDescriptor } - if let outputRead = output.getReadFileDescriptor() { + if let outputRead = output.readFileDescriptor { // Set parent side to be uninhertable SetHandleInformation( outputRead.platformDescriptor, @@ -805,10 +833,10 @@ extension Subprocess.Configuration { ) } // Error - if let errorWrite = error.getWriteFileDescriptor() { + if let errorWrite = error.writeFileDescriptor { info.hStdError = errorWrite.platformDescriptor } - if let errorRead = error.getReadFileDescriptor() { + if let errorRead = error.readFileDescriptor { // Set parent side to be uninhertable SetHandleInformation( errorRead.platformDescriptor, @@ -834,12 +862,12 @@ extension Subprocess.Configuration { // actually resolve the path. However, to maintain // the same behavior as other platforms, still check // here to make sure the executable actually exists - guard self.executable.resolveExecutablePath( - withPathValue: self.environment.pathValue() - ) != nil else { - throw CocoaError(.executableNotLoadable, userInfo: [ - .debugDescriptionErrorKey : "\(self.executable.description) is not an executable" - ]) + do { + _ = try self.executable.resolveExecutablePath( + withPathValue: self.environment.pathValue() + ) + } catch { + throw error } executableNameOrPath = name } @@ -876,7 +904,7 @@ extension Subprocess.Configuration { func quoteWindowsCommandArg(arg: String) -> String { // Windows escaping, adapted from Daniel Colascione's "Everyone quotes // command line arguments the wrong way" - Microsoft Developer Blog - if !arg.contains(where: {" \t\n\"".contains($0)}) { + if !arg.contains(where: { " \t\n\"".contains($0) }) { return arg } @@ -911,7 +939,7 @@ extension Subprocess.Configuration { break } let backslashCount = unquoted.distance(from: unquoted.startIndex, to: firstNonBackslash) - if (unquoted[firstNonBackslash] == "\"") { + if unquoted[firstNonBackslash] == "\"" { // This is a string of \ followed by a " e.g. foo\"bar. Escape the // backslashes and the quote quoted.append(String(repeating: "\\", count: backslashCount * 2 + 1)) @@ -939,20 +967,7 @@ extension Subprocess.Configuration { } // MARK: - PlatformFileDescriptor Type -extension Subprocess { - internal typealias PlatformFileDescriptor = HANDLE -} - -// MARK: - Read Buffer Size -extension Subprocess { - @inline(__always) - internal static var readBufferSize: Int { - // FIXME: Use Platform.pageSize here - var sysInfo: SYSTEM_INFO = SYSTEM_INFO() - GetSystemInfo(&sysInfo) - return Int(sysInfo.dwPageSize) - } -} +internal typealias PlatformFileDescriptor = HANDLE // MARK: - Pipe Support extension FileDescriptor { @@ -968,13 +983,14 @@ extension FileDescriptor { var readHandle: HANDLE? = nil var writeHandle: HANDLE? = nil guard CreatePipe(&readHandle, &writeHandle, &saAttributes, 0), - readHandle != INVALID_HANDLE_VALUE, - writeHandle != INVALID_HANDLE_VALUE, - let readHandle: HANDLE = readHandle, - let writeHandle: HANDLE = writeHandle else { - throw CocoaError.windowsError( - underlying: GetLastError(), - errorCode: .fileReadUnknown + readHandle != INVALID_HANDLE_VALUE, + writeHandle != INVALID_HANDLE_VALUE, + let readHandle: HANDLE = readHandle, + let writeHandle: HANDLE = writeHandle + else { + throw SubprocessError( + code: .init(.failedToCreatePipe), + underlyingError: .init(rawValue: GetLastError()) ) } let readFd = _open_osfhandle( @@ -992,147 +1008,138 @@ extension FileDescriptor { ) } - internal static func openDevNull( - withAcessMode mode: FileDescriptor.AccessMode - ) throws -> FileDescriptor { - return try "NUL".withPlatformString { - let handle = CreateFileW( - $0, - DWORD(GENERIC_WRITE), - DWORD(FILE_SHARE_WRITE), - nil, - DWORD(OPEN_EXISTING), - DWORD(FILE_ATTRIBUTE_NORMAL), - nil - ) - guard let handle = handle, - handle != INVALID_HANDLE_VALUE else { - throw CocoaError.windowsError( - underlying: GetLastError(), - errorCode: .fileReadUnknown - ) - } - let devnull = _open_osfhandle( - intptr_t(bitPattern: handle), - mode.rawValue - ) - return FileDescriptor(rawValue: devnull) - } - } - - var platformDescriptor: Subprocess.PlatformFileDescriptor { + var platformDescriptor: PlatformFileDescriptor { return HANDLE(bitPattern: _get_osfhandle(self.rawValue))! } - internal func read(upToLength maxLength: Int) async throws -> Data { - // TODO: Figure out a better way to asynchornously read + internal func readChunk(upToLength maxLength: Int) async throws -> SequenceOutput.Buffer? { return try await withCheckedThrowingContinuation { continuation in - DispatchQueue.global(qos: .userInitiated).async { - var totalBytesRead: Int = 0 - var lastError: DWORD? = nil - let values = Array( - unsafeUninitializedCapacity: maxLength - ) { buffer, initializedCount in - while true { - guard let baseAddress = buffer.baseAddress else { - initializedCount = 0 - break - } - let bufferPtr = baseAddress.advanced(by: totalBytesRead) - var bytesRead: DWORD = 0 - let readSucceed = ReadFile( - self.platformDescriptor, - UnsafeMutableRawPointer(mutating: bufferPtr), - DWORD(maxLength - totalBytesRead), - &bytesRead, - nil - ) - if !readSucceed { - // Windows throws ERROR_BROKEN_PIPE when the pipe is closed - let error = GetLastError() - if error == ERROR_BROKEN_PIPE { - // We are done reading - initializedCount = totalBytesRead - } else { - // We got some error - lastError = error - initializedCount = 0 - } - break - } else { - // We succesfully read the current round - totalBytesRead += Int(bytesRead) - } - - if totalBytesRead >= maxLength { - initializedCount = min(maxLength, totalBytesRead) - break - } - } - } - if let lastError = lastError { - continuation.resume(throwing: CocoaError.windowsError( - underlying: lastError, - errorCode: .fileReadUnknown) - ) - } else { - continuation.resume(returning: Data(values)) + self.readUntilEOF( + upToLength: maxLength + ) { result in + switch result { + case .failure(let error): + continuation.resume(throwing: error) + case .success(let bytes): + continuation.resume(returning: SequenceOutput.Buffer(data: bytes)) } } } } - internal func write(_ data: S) async throws where S.Element == UInt8 { - // TODO: Figure out a better way to asynchornously write - try await withCheckedThrowingContinuation { ( - continuation: CheckedContinuation - ) -> Void in - DispatchQueue.global(qos: .userInitiated).async { - let buffer = Array(data) - buffer.withUnsafeBytes { ptr in - var writtenBytes: DWORD = 0 - let writeSucceed = WriteFile( + internal func readUntilEOF( + upToLength maxLength: Int, + resultHandler: @Sendable @escaping (Swift.Result<[UInt8], any (Error & Sendable)>) -> Void + ) { + DispatchQueue.global(qos: .userInitiated).async { + var totalBytesRead: Int = 0 + var lastError: DWORD? = nil + let values = [UInt8]( + unsafeUninitializedCapacity: maxLength + ) { buffer, initializedCount in + while true { + guard let baseAddress = buffer.baseAddress else { + initializedCount = 0 + break + } + let bufferPtr = baseAddress.advanced(by: totalBytesRead) + var bytesRead: DWORD = 0 + let readSucceed = ReadFile( self.platformDescriptor, - ptr.baseAddress, - DWORD(buffer.count), - &writtenBytes, + UnsafeMutableRawPointer(mutating: bufferPtr), + DWORD(maxLength - totalBytesRead), + &bytesRead, nil ) - if !writeSucceed { - continuation.resume(throwing: CocoaError.windowsError( - underlying: GetLastError(), - errorCode: .fileWriteUnknown) - ) + if !readSucceed { + // Windows throws ERROR_BROKEN_PIPE when the pipe is closed + let error = GetLastError() + if error == ERROR_BROKEN_PIPE { + // We are done reading + initializedCount = totalBytesRead + } else { + // We got some error + lastError = error + initializedCount = 0 + } + break } else { - continuation.resume() + // We succesfully read the current round + totalBytesRead += Int(bytesRead) + } + + if totalBytesRead >= maxLength { + initializedCount = min(maxLength, totalBytesRead) + break } } } + if let lastError = lastError { + let windowsError = SubprocessError( + code: .init(.failedToReadFromSubprocess), + underlyingError: .init(rawValue: lastError) + ) + resultHandler(.failure(windowsError)) + } else { + resultHandler(.success(values)) + } } } -} - -extension String { - static let debugDescriptionErrorKey = "DebugDescription" -} -// MARK: - CocoaError + Win32 -internal let NSUnderlyingErrorKey = "NSUnderlyingError" + internal func write( + _ array: [UInt8] + ) async throws -> Int { + try await withCheckedThrowingContinuation { continuation in + // TODO: Figure out a better way to asynchornously write + DispatchQueue.global(qos: .userInitiated).async { + array.withUnsafeBytes { + self.write($0) { writtenLength, error in + if let error = error { + continuation.resume(throwing: error) + } else { + continuation.resume(returning: writtenLength) + } + } + } + } + } + } -extension CocoaError { - static func windowsError(underlying: DWORD, errorCode: Code) -> CocoaError { - let userInfo = [ - NSUnderlyingErrorKey : Win32Error(underlying) - ] - return CocoaError(errorCode, userInfo: userInfo) + package func write( + _ ptr: UnsafeRawBufferPointer, + completion: @escaping (Int, Swift.Error?) -> Void + ) { + func _write( + _ ptr: UnsafeRawBufferPointer, + count: Int, + completion: @escaping (Int, Swift.Error?) -> Void + ) { + var writtenBytes: DWORD = 0 + let writeSucceed = WriteFile( + self.platformDescriptor, + ptr.baseAddress, + DWORD(count), + &writtenBytes, + nil + ) + if !writeSucceed { + let error = SubprocessError( + code: .init(.failedToWriteToSubprocess), + underlyingError: .init(rawValue: GetLastError()) + ) + completion(Int(writtenBytes), error) + } else { + completion(Int(writtenBytes), nil) + } + } } } -private extension Optional where Wrapped == String { - func withOptionalCString( +extension Optional where Wrapped == String { + fileprivate func withOptionalCString( encodedAs targetEncoding: Encoding.Type, _ body: (UnsafePointer?) throws -> Result - ) rethrows -> Result where Encoding : _UnicodeEncoding { + ) rethrows -> Result where Encoding: _UnicodeEncoding { switch self { case .none: return try body(nil) @@ -1141,7 +1148,7 @@ private extension Optional where Wrapped == String { } } - func withOptionalNTPathRepresentation( + fileprivate func withOptionalNTPathRepresentation( _ body: (UnsafePointer?) throws -> Result ) throws -> Result { switch self { @@ -1159,23 +1166,30 @@ extension String { _ body: (UnsafePointer) throws -> Result ) throws -> Result { guard !isEmpty else { - throw CocoaError(.fileReadInvalidFileName) + throw SubprocessError( + code: .init(.invalidWindowsPath(self)), + underlyingError: nil + ) } var iter = self.utf8.makeIterator() - let bLeadingSlash = if [._slash, ._backslash].contains(iter.next()), iter.next()?.isLetter ?? false, iter.next() == ._colon { true } else { false } + let bLeadingSlash = + if [._slash, ._backslash].contains(iter.next()), iter.next()?.isLetter ?? false, iter.next() == ._colon { + true + } else { false } // Strip the leading `/` on a RFC8089 path (`/[drive-letter]:/...` ). A // leading slash indicates a rooted path on the drive for the current // working directory. - return try Substring(self.utf8.dropFirst(bLeadingSlash ? 1 : 0)).withCString(encodedAs: UTF16.self) { pwszPath in + return try Substring(self.utf8.dropFirst(bLeadingSlash ? 1 : 0)).withCString(encodedAs: UTF16.self) { + pwszPath in // 1. Normalize the path first. let dwLength: DWORD = GetFullPathNameW(pwszPath, 0, nil, nil) return try withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: Int(dwLength)) { guard GetFullPathNameW(pwszPath, DWORD($0.count), $0.baseAddress, nil) > 0 else { - throw CocoaError.windowsError( - underlying: GetLastError(), - errorCode: .fileReadUnknown + throw SubprocessError( + code: .init(.invalidWindowsPath(self)), + underlyingError: .init(rawValue: GetLastError()) ) } @@ -1186,27 +1200,22 @@ extension String { } } -struct Win32Error: Error { - public typealias Code = DWORD - public let code: Code - - public static var errorDomain: String { - return "NSWin32ErrorDomain" - } - - public init(_ code: Code) { - self.code = code - } -} - -internal extension UInt8 { +extension UInt8 { static var _slash: UInt8 { UInt8(ascii: "/") } static var _backslash: UInt8 { UInt8(ascii: "\\") } static var _colon: UInt8 { UInt8(ascii: ":") } var isLetter: Bool? { - return (0x41 ... 0x5a) ~= self || (0x61 ... 0x7a) ~= self + return (0x41...0x5a) ~= self || (0x61...0x7a) ~= self + } +} + +extension OutputProtocol { + internal func output(from data: [UInt8]) throws -> OutputType { + return try data.withUnsafeBytes { + return try self.output(from: $0) + } } } -#endif // canImport(WinSDK) +#endif // canImport(WinSDK) diff --git a/Sources/_Subprocess/Result.swift b/Sources/_Subprocess/Result.swift new file mode 100644 index 000000000..e1f798940 --- /dev/null +++ b/Sources/_Subprocess/Result.swift @@ -0,0 +1,123 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +#if canImport(System) +import System +#else +@preconcurrency import SystemPackage +#endif + +// MARK: - Result + +/// A simple wrapper around the generic result returned by the +/// `run` closures with the corresponding `TerminationStatus` +/// of the child process. +public struct ExecutionResult { + /// The termination status of the child process + public let terminationStatus: TerminationStatus + /// The result returned by the closure passed to `.run` methods + public let value: Result + + internal init(terminationStatus: TerminationStatus, value: Result) { + self.terminationStatus = terminationStatus + self.value = value + } +} + +/// The result of a subprocess execution with its collected +/// standard output and standard error. +public struct CollectedResult< + Output: OutputProtocol, + Error: OutputProtocol +>: Sendable { + /// The process identifier for the executed subprocess + public let processIdentifier: ProcessIdentifier + /// The termination status of the executed subprocess + public let terminationStatus: TerminationStatus + public let standardOutput: Output.OutputType + public let standardError: Error.OutputType + + internal init( + processIdentifier: ProcessIdentifier, + terminationStatus: TerminationStatus, + standardOutput: Output.OutputType, + standardError: Error.OutputType + ) { + self.processIdentifier = processIdentifier + self.terminationStatus = terminationStatus + self.standardOutput = standardOutput + self.standardError = standardError + } +} + +// MARK: - CollectedResult Conformances +extension CollectedResult: Equatable where Output.OutputType: Equatable, Error.OutputType: Equatable {} + +extension CollectedResult: Hashable where Output.OutputType: Hashable, Error.OutputType: Hashable {} + +extension CollectedResult: Codable where Output.OutputType: Codable, Error.OutputType: Codable {} + +extension CollectedResult: CustomStringConvertible +where Output.OutputType: CustomStringConvertible, Error.OutputType: CustomStringConvertible { + public var description: String { + return """ + CollectedResult( + processIdentifier: \(self.processIdentifier), + terminationStatus: \(self.terminationStatus.description), + standardOutput: \(self.standardOutput.description) + standardError: \(self.standardError.description) + ) + """ + } +} + +extension CollectedResult: CustomDebugStringConvertible +where Output.OutputType: CustomDebugStringConvertible, Error.OutputType: CustomDebugStringConvertible { + public var debugDescription: String { + return """ + CollectedResult( + processIdentifier: \(self.processIdentifier), + terminationStatus: \(self.terminationStatus.description), + standardOutput: \(self.standardOutput.debugDescription) + standardError: \(self.standardError.debugDescription) + ) + """ + } +} + +// MARK: - ExecutionResult Conformances +extension ExecutionResult: Equatable where Result: Equatable {} + +extension ExecutionResult: Hashable where Result: Hashable {} + +extension ExecutionResult: Codable where Result: Codable {} + +extension ExecutionResult: CustomStringConvertible where Result: CustomStringConvertible { + public var description: String { + return """ + ExecutionResult( + terminationStatus: \(self.terminationStatus.description), + value: \(self.value.description) + ) + """ + } +} + +extension ExecutionResult: CustomDebugStringConvertible where Result: CustomDebugStringConvertible { + public var debugDescription: String { + return """ + ExecutionResult( + terminationStatus: \(self.terminationStatus.debugDescription), + value: \(self.value.debugDescription) + ) + """ + } +} diff --git a/Sources/_Subprocess/Subprocess+API.swift b/Sources/_Subprocess/Subprocess+API.swift deleted file mode 100644 index f9c8b1ecb..000000000 --- a/Sources/_Subprocess/Subprocess+API.swift +++ /dev/null @@ -1,465 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift.org project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift.org project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import SystemPackage -#if canImport(FoundationEssentials) -import FoundationEssentials -#elseif canImport(Foundation) -import Foundation -#endif - -extension Subprocess { - /// Run a executable with given parameters and capture its - /// standard output and standard error. - /// - Parameters: - /// - executable: The executable to run. - /// - arguments: The arguments to pass to the executable. - /// - environment: The environment to use for the process. - /// - workingDirectory: The working directory to use for the subprocess. - /// - platformOptions: The platform specific options to use - /// when running the executable. - /// - input: The input to send to the executable. - /// - output: The method to use for collecting the standard output. - /// - error: The method to use for collecting the standard error. - /// - Returns: `CollectedResult` which contains process identifier, - /// termination status, captured standard output and standard error. - public static func run( - _ executable: Executable, - arguments: Arguments = [], - environment: Environment = .inherit, - workingDirectory: FilePath? = nil, - platformOptions: PlatformOptions = PlatformOptions(), - input: InputMethod = .noInput, - output: CollectedOutputMethod = .collect(), - error: CollectedOutputMethod = .collect() - ) async throws -> CollectedResult { - let result = try await self.run( - executable, - arguments: arguments, - environment: environment, - workingDirectory: workingDirectory, - platformOptions: platformOptions, - input: input, - output: .init(method: output.method), - error: .init(method: error.method) - ) { subprocess in - let (standardOutput, standardError) = try await subprocess.captureIOs() - return ( - processIdentifier: subprocess.processIdentifier, - standardOutput: standardOutput, - standardError: standardError - ) - } - return CollectedResult( - processIdentifier: result.value.processIdentifier, - terminationStatus: result.terminationStatus, - standardOutput: result.value.standardOutput, - standardError: result.value.standardError - ) - } - - /// Run a executable with given parameters and capture its - /// standard output and standard error. - /// - Parameters: - /// - executable: The executable to run. - /// - arguments: The arguments to pass to the executable. - /// - environment: The environment to use for the process. - /// - workingDirectory: The working directory to use for the subprocess. - /// - platformOptions: The platform specific options to use - /// when running the executable. - /// - input: The input to send to the executable. - /// - output: The method to use for collecting the standard output. - /// - error: The method to use for collecting the standard error. - /// - Returns: `CollectedResult` which contains process identifier, - /// termination status, captured standard output and standard error. - public static func run( - _ executable: Executable, - arguments: Arguments = [], - environment: Environment = .inherit, - workingDirectory: FilePath? = nil, - platformOptions: PlatformOptions = PlatformOptions(), - input: some Sequence, - output: CollectedOutputMethod = .collect(), - error: CollectedOutputMethod = .collect() - ) async throws -> CollectedResult { - let result = try await self.run( - executable, - arguments: arguments, - environment: environment, - workingDirectory: workingDirectory, - platformOptions: platformOptions, - output: .init(method: output.method), - error: .init(method: output.method) - ) { subprocess, writer in - return try await withThrowingTaskGroup(of: CapturedIOs?.self) { group in - group.addTask { - try await writer.write(input) - try await writer.finish() - return nil - } - group.addTask { - return try await subprocess.captureIOs() - } - var capturedIOs: CapturedIOs! - while let result = try await group.next() { - if result != nil { - capturedIOs = result - } - } - return ( - processIdentifier: subprocess.processIdentifier, - standardOutput: capturedIOs.standardOutput, - standardError: capturedIOs.standardError - ) - } - } - return CollectedResult( - processIdentifier: result.value.processIdentifier, - terminationStatus: result.terminationStatus, - standardOutput: result.value.standardOutput, - standardError: result.value.standardError - ) - } - - /// Run a executable with given parameters and capture its - /// standard output and standard error. - /// - Parameters: - /// - executable: The executable to run. - /// - arguments: The arguments to pass to the executable. - /// - environment: The environment to use for the process. - /// - workingDirectory: The working directory to use for the subprocess. - /// - platformOptions: The platform specific options to use - /// when running the executable. - /// - input: The input to send to the executable. - /// - output: The method to use for collecting the standard output. - /// - error: The method to use for collecting the standard error. - /// - Returns: `CollectedResult` which contains process identifier, - /// termination status, captured standard output and standard error. - public static func run( - _ executable: Executable, - arguments: Arguments = [], - environment: Environment = .inherit, - workingDirectory: FilePath? = nil, - platformOptions: PlatformOptions = PlatformOptions(), - input: S, - output: CollectedOutputMethod = .collect(), - error: CollectedOutputMethod = .collect() - ) async throws -> CollectedResult where S.Element == UInt8 { - let result = try await self.run( - executable, - arguments: arguments, - environment: environment, - workingDirectory: workingDirectory, - platformOptions: platformOptions, - output: .init(method: output.method), - error: .init(method: output.method) - ) { subprocess, writer in - return try await withThrowingTaskGroup(of: CapturedIOs?.self) { group in - group.addTask { - try await writer.write(input) - try await writer.finish() - return nil - } - group.addTask { - return try await subprocess.captureIOs() - } - var capturedIOs: CapturedIOs! - while let result = try await group.next() { - capturedIOs = result - } - return ( - processIdentifier: subprocess.processIdentifier, - standardOutput: capturedIOs.standardOutput, - standardError: capturedIOs.standardError - ) - } - } - return CollectedResult( - processIdentifier: result.value.processIdentifier, - terminationStatus: result.terminationStatus, - standardOutput: result.value.standardOutput, - standardError: result.value.standardError - ) - } -} - -// MARK: Custom Execution Body -extension Subprocess { - /// Run a executable with given parameters and a custom closure - /// to manage the running subprocess' lifetime and its IOs. - /// - Parameters: - /// - executable: The executable to run. - /// - arguments: The arguments to pass to the executable. - /// - environment: The environment in which to run the executable. - /// - workingDirectory: The working directory in which to run the executable. - /// - platformOptions: The platform specific options to use - /// when running the executable. - /// - input: The input to send to the executable. - /// - output: The method to use for redirecting the standard output. - /// - error: The method to use for redirecting the standard error. - /// - body: The custom execution body to manually control the running process - /// - Returns a ExecutableResult type containing the return value - /// of the closure. - public static func run( - _ executable: Executable, - arguments: Arguments = [], - environment: Environment = .inherit, - workingDirectory: FilePath? = nil, - platformOptions: PlatformOptions = PlatformOptions(), - input: InputMethod = .noInput, - output: RedirectedOutputMethod = .redirectToSequence, - error: RedirectedOutputMethod = .redirectToSequence, - _ body: (sending @escaping (Subprocess) async throws -> R) - ) async throws -> ExecutionResult { - return try await Configuration( - executable: executable, - arguments: arguments, - environment: environment, - workingDirectory: workingDirectory, - platformOptions: platformOptions - ) - .run(input: input, output: output, error: error, body) - } - - /// Run a executable with given parameters and a custom closure - /// to manage the running subprocess' lifetime and its IOs. - /// - Parameters: - /// - executable: The executable to run. - /// - arguments: The arguments to pass to the executable. - /// - environment: The environment in which to run the executable. - /// - workingDirectory: The working directory in which to run the executable. - /// - platformOptions: The platform specific options to use - /// when running the executable. - /// - input: The input to send to the executable. - /// - output: The method to use for redirecting the standard output. - /// - error: The method to use for redirecting the standard error. - /// - body: The custom execution body to manually control the running process - /// - Returns a `ExecutableResult` type containing the return value - /// of the closure. - public static func run( - _ executable: Executable, - arguments: Arguments = [], - environment: Environment = .inherit, - workingDirectory: FilePath? = nil, - platformOptions: PlatformOptions = PlatformOptions(), - input: some Sequence, - output: RedirectedOutputMethod = .redirectToSequence, - error: RedirectedOutputMethod = .redirectToSequence, - _ body: (sending @escaping (Subprocess) async throws -> R) - ) async throws -> ExecutionResult { - return try await Configuration( - executable: executable, - arguments: arguments, - environment: environment, - workingDirectory: workingDirectory, - platformOptions: platformOptions - ) - .run(output: output, error: error) { execution, writer in - return try await withThrowingTaskGroup(of: R?.self) { group in - group.addTask { - try await writer.write(input) - try await writer.finish() - return nil - } - group.addTask { - return try await body(execution) - } - var result: R! - while let next = try await group.next() { - result = next - } - return result - } - } - } - - /// Run a executable with given parameters and a custom closure - /// to manage the running subprocess' lifetime and its IOs. - /// - Parameters: - /// - executable: The executable to run. - /// - arguments: The arguments to pass to the executable. - /// - environment: The environment in which to run the executable. - /// - workingDirectory: The working directory in which to run the executable. - /// - platformOptions: The platform specific options to use - /// when running the executable. - /// - input: The input to send to the executable. - /// - output: The method to use for redirecting the standard output. - /// - error: The method to use for redirecting the standard error. - /// - body: The custom execution body to manually control the running process - /// - Returns a `ExecutableResult` type containing the return value - /// of the closure. - public static func run( - _ executable: Executable, - arguments: Arguments = [], - environment: Environment = .inherit, - workingDirectory: FilePath? = nil, - platformOptions: PlatformOptions = PlatformOptions(), - input: S, - output: RedirectedOutputMethod = .redirectToSequence, - error: RedirectedOutputMethod = .redirectToSequence, - _ body: (sending @escaping (Subprocess) async throws -> R) - ) async throws -> ExecutionResult where S.Element == UInt8 { - return try await Configuration( - executable: executable, - arguments: arguments, - environment: environment, - workingDirectory: workingDirectory, - platformOptions: platformOptions - ) - .run(output: output, error: error) { execution, writer in - return try await withThrowingTaskGroup(of: R?.self) { group in - group.addTask { - try await writer.write(input) - try await writer.finish() - return nil - } - group.addTask { - return try await body(execution) - } - var result: R! - while let next = try await group.next() { - result = next - } - return result - } - } - } - - /// Run a executable with given parameters and a custom closure - /// to manage the running subprocess' lifetime and write to its - /// standard input via `StandardInputWriter` - /// - Parameters: - /// - executable: The executable to run. - /// - arguments: The arguments to pass to the executable. - /// - environment: The environment in which to run the executable. - /// - workingDirectory: The working directory in which to run the executable. - /// - platformOptions: The platform specific options to use - /// when running the executable. - /// - output: The method to use for redirecting the standard output. - /// - error: The method to use for redirecting the standard error. - /// - body: The custom execution body to manually control the running process - /// - Returns a ExecutableResult type containing the return value - /// of the closure. - public static func run( - _ executable: Executable, - arguments: Arguments = [], - environment: Environment = .inherit, - workingDirectory: FilePath? = nil, - platformOptions: PlatformOptions = PlatformOptions(), - output: RedirectedOutputMethod = .redirectToSequence, - error: RedirectedOutputMethod = .redirectToSequence, - _ body: (sending @escaping (Subprocess, StandardInputWriter) async throws -> R) - ) async throws -> ExecutionResult { - return try await Configuration( - executable: executable, - arguments: arguments, - environment: environment, - workingDirectory: workingDirectory, - platformOptions: platformOptions - ) - .run(output: output, error: error, body) - } -} - -// MARK: - Configuration Based -extension Subprocess { - /// Run a executable with given parameters specified by a - /// `Subprocess.Configuration` - /// - Parameters: - /// - configuration: The `Subprocess` configuration to run. - /// - output: The method to use for redirecting the standard output. - /// - error: The method to use for redirecting the standard error. - /// - body: The custom configuration body to manually control - /// the running process and write to its standard input. - /// - Returns a ExecutableResult type containing the return value - /// of the closure. - public static func run( - _ configuration: Configuration, - output: RedirectedOutputMethod = .redirectToSequence, - error: RedirectedOutputMethod = .redirectToSequence, - _ body: (sending @escaping (Subprocess, StandardInputWriter) async throws -> R) - ) async throws -> ExecutionResult { - return try await configuration.run(output: output, error: error, body) - } -} - -// MARK: - Detached -extension Subprocess { - /// Run a executable with given parameters and return its process - /// identifier immediately without monitoring the state of the - /// subprocess nor waiting until it exits. - /// - /// This method is useful for launching subprocesses that outlive their - /// parents (for example, daemons and trampolines). - /// - /// - Parameters: - /// - executable: The executable to run. - /// - arguments: The arguments to pass to the executable. - /// - environment: The environment to use for the process. - /// - workingDirectory: The working directory for the process. - /// - platformOptions: The platform specific options to use for the process. - /// - input: A file descriptor to bind to the subprocess' standard input. - /// - output: A file descriptor to bind to the subprocess' standard output. - /// - error: A file descriptor to bind to the subprocess' standard error. - /// - Returns: the process identifier for the subprocess. - public static func runDetached( - _ executable: Executable, - arguments: Arguments = [], - environment: Environment = .inherit, - workingDirectory: FilePath? = nil, - platformOptions: PlatformOptions = PlatformOptions(), - input: FileDescriptor? = nil, - output: FileDescriptor? = nil, - error: FileDescriptor? = nil - ) throws -> ProcessIdentifier { - // Create input - let executionInput: ExecutionInput - let executionOutput: ExecutionOutput - let executionError: ExecutionOutput - if let inputFd = input { - executionInput = .init(storage: .fileDescriptor(inputFd, false)) - } else { - let devnull: FileDescriptor = try .openDevNull(withAcessMode: .readOnly) - executionInput = .init(storage: .noInput(devnull)) - } - if let outputFd = output { - executionOutput = .init(storage: .fileDescriptor(outputFd, false)) - } else { - let devnull: FileDescriptor = try .openDevNull(withAcessMode: .writeOnly) - executionOutput = .init(storage: .discarded(devnull)) - } - if let errorFd = error { - executionError = .init( - storage: .fileDescriptor(errorFd, false) - ) - } else { - let devnull: FileDescriptor = try .openDevNull(withAcessMode: .writeOnly) - executionError = .init(storage: .discarded(devnull)) - } - // Spawn! - let config: Configuration = Configuration( - executable: executable, - arguments: arguments, - environment: environment, - workingDirectory: workingDirectory, - platformOptions: platformOptions - ) - return try config.spawn( - withInput: executionInput, - output: executionOutput, - error: executionError - ).processIdentifier - } -} - diff --git a/Sources/_Subprocess/Subprocess+AsyncDataSequence.swift b/Sources/_Subprocess/Subprocess+AsyncDataSequence.swift deleted file mode 100644 index a3a3e3936..000000000 --- a/Sources/_Subprocess/Subprocess+AsyncDataSequence.swift +++ /dev/null @@ -1,86 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift.org project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift.org project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import SystemPackage -import Dispatch - -#if canImport(FoundationEssentials) -import FoundationEssentials -#elseif canImport(Foundation) -import Foundation -#endif - -extension Subprocess { - public struct AsyncDataSequence: AsyncSequence, Sendable, _AsyncSequence { - public typealias Error = any Swift.Error - - public typealias Element = Data - - @_nonSendable - public struct Iterator: AsyncIteratorProtocol { - public typealias Element = Data - - private let fileDescriptor: FileDescriptor - private var buffer: [UInt8] - private var currentPosition: Int - private var finished: Bool - - internal init(fileDescriptor: FileDescriptor) { - self.fileDescriptor = fileDescriptor - self.buffer = [] - self.currentPosition = 0 - self.finished = false - } - - public mutating func next() async throws -> Data? { - let data = try await self.fileDescriptor.readChunk( - upToLength: Subprocess.readBufferSize - ) - if data == nil { - // We finished reading. Close the file descriptor now - try self.fileDescriptor.close() - return nil - } - return data - } - } - - private let fileDescriptor: FileDescriptor - - init(fileDescriptor: FileDescriptor) { - self.fileDescriptor = fileDescriptor - } - - public func makeAsyncIterator() -> Iterator { - return Iterator(fileDescriptor: self.fileDescriptor) - } - } -} - -extension RangeReplaceableCollection { - /// Creates a new instance of a collection containing the elements of an asynchronous sequence. - /// - /// - Parameter source: The asynchronous sequence of elements for the new collection. - @inlinable - public init(_ source: Source) async rethrows where Source.Element == Element { - self.init() - for try await item in source { - append(item) - } - } -} - -public protocol _AsyncSequence: AsyncSequence { - associatedtype Error -} diff --git a/Sources/_Subprocess/Subprocess+Configuration.swift b/Sources/_Subprocess/Subprocess+Configuration.swift deleted file mode 100644 index 0b7857903..000000000 --- a/Sources/_Subprocess/Subprocess+Configuration.swift +++ /dev/null @@ -1,769 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift.org project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift.org project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -@preconcurrency import SystemPackage - -#if canImport(Darwin) -import Darwin -#elseif canImport(Glibc) -import Glibc -#elseif canImport(WinSDK) -import WinSDK -#endif - -#if canImport(FoundationEssentials) -import FoundationEssentials -#elseif canImport(Foundation) -import Foundation -#endif - -extension Subprocess { - /// A collection of configurations parameters to use when - /// spawning a subprocess. - public struct Configuration: Sendable, Hashable { - - internal enum RunState: Sendable { - case workBody(Result) - case monitorChildProcess(TerminationStatus) - } - - /// The executable to run. - public var executable: Executable - /// The arguments to pass to the executable. - public var arguments: Arguments - /// The environment to use when running the executable. - public var environment: Environment - /// The working directory to use when running the executable. - public var workingDirectory: FilePath - /// The platform specifc options to use when - /// running the subprocess. - public var platformOptions: PlatformOptions - - public init( - executable: Executable, - arguments: Arguments = [], - environment: Environment = .inherit, - workingDirectory: FilePath? = nil, - platformOptions: PlatformOptions = PlatformOptions() - ) { - self.executable = executable - self.arguments = arguments - self.environment = environment - self.workingDirectory = workingDirectory ?? .currentWorkingDirectory - self.platformOptions = platformOptions - } - - /// Close each input individually, and throw the first error if there's multiple errors thrown - @Sendable - private func cleanup( - process: Subprocess, - childSide: Bool, parentSide: Bool, - attemptToTerminateSubProcess: Bool - ) async throws { - guard childSide || parentSide || attemptToTerminateSubProcess else { - return - } - - // Attempt to teardown the subprocess - if attemptToTerminateSubProcess { - await process.teardown( - using: self.platformOptions.teardownSequence - ) - } - - let inputCloseFunc: () throws -> Void - let outputCloseFunc: () throws -> Void - let errorCloseFunc: () throws -> Void - if childSide && parentSide { - // Close all - inputCloseFunc = process.executionInput.closeAll - outputCloseFunc = process.executionOutput.closeAll - errorCloseFunc = process.executionError.closeAll - } else if childSide { - // Close child only - inputCloseFunc = process.executionInput.closeChildSide - outputCloseFunc = process.executionOutput.closeChildSide - errorCloseFunc = process.executionError.closeChildSide - } else { - // Close parent only - inputCloseFunc = process.executionInput.closeParentSide - outputCloseFunc = process.executionOutput.closeParentSide - errorCloseFunc = process.executionError.closeParentSide - } - - var inputError: Error? - var outputError: Error? - var errorError: Error? // lol - do { - try inputCloseFunc() - } catch { - inputError = error - } - - do { - try outputCloseFunc() - } catch { - outputError = error - } - - do { - try errorCloseFunc() - } catch { - errorError = error // lolol - } - - if let inputError = inputError { - throw inputError - } - - if let outputError = outputError { - throw outputError - } - - if let errorError = errorError { - throw errorError - } - } - - /// Close each input individually, and throw the first error if there's multiple errors thrown - @Sendable - internal func cleanupAll( - input: ExecutionInput, - output: ExecutionOutput, - error: ExecutionOutput - ) throws { - var inputError: Error? - var outputError: Error? - var errorError: Error? - - do { - try input.closeAll() - } catch { - inputError = error - } - - do { - try output.closeAll() - } catch { - outputError = error - } - - do { - try error.closeAll() - } catch { - errorError = error - } - - if let inputError = inputError { - throw inputError - } - if let outputError = outputError { - throw outputError - } - if let errorError = errorError { - throw errorError - } - } - - internal func run( - output: RedirectedOutputMethod, - error: RedirectedOutputMethod, - _ body: sending @escaping (Subprocess, StandardInputWriter) async throws -> R - ) async throws -> ExecutionResult { - let (readFd, writeFd) = try FileDescriptor.pipe() - let executionInput: ExecutionInput = .init(storage: .customWrite(readFd, writeFd)) - let executionOutput: ExecutionOutput = try output.createExecutionOutput() - let executionError: ExecutionOutput = try error.createExecutionOutput() - let process: Subprocess = try self.spawn( - withInput: executionInput, - output: executionOutput, - error: executionError) - // After spawn, cleanup child side fds - try await self.cleanup( - process: process, - childSide: true, - parentSide: false, - attemptToTerminateSubProcess: false - ) - return try await withAsyncTaskCancellationHandler { - return try await withThrowingTaskGroup(of: RunState.self) { group in - group.addTask { - let status = try await monitorProcessTermination( - forProcessWithIdentifier: process.processIdentifier) - return .monitorChildProcess(status) - } - group.addTask { - do { - let result = try await body(process, .init(input: executionInput)) - try await self.cleanup( - process: process, - childSide: false, - parentSide: true, - attemptToTerminateSubProcess: false - ) - return .workBody(result) - } catch { - // Cleanup everything - try await self.cleanup( - process: process, - childSide: false, - parentSide: true, - attemptToTerminateSubProcess: false - ) - throw error - } - } - - var result: R! - var terminationStatus: TerminationStatus! - while let state = try await group.next() { - switch state { - case .monitorChildProcess(let status): - // We don't really care about termination status here - terminationStatus = status - case .workBody(let workResult): - result = workResult - } - } - return ExecutionResult(terminationStatus: terminationStatus, value: result) - } - } onCancel: { - // Attempt to terminate the child process - // Since the task has already been cancelled, - // this is the best we can do - try? await self.cleanup( - process: process, - childSide: true, - parentSide: true, - attemptToTerminateSubProcess: true - ) - } - } - - internal func run( - input: InputMethod, - output: RedirectedOutputMethod, - error: RedirectedOutputMethod, - _ body: (sending @escaping (Subprocess) async throws -> R) - ) async throws -> ExecutionResult { - let executionInput = try input.createExecutionInput() - let executionOutput = try output.createExecutionOutput() - let executionError = try error.createExecutionOutput() - let process = try self.spawn( - withInput: executionInput, - output: executionOutput, - error: executionError) - // After spawn, clean up child side - try await self.cleanup( - process: process, - childSide: true, - parentSide: false, - attemptToTerminateSubProcess: false - ) - return try await withAsyncTaskCancellationHandler { - return try await withThrowingTaskGroup(of: RunState.self) { group in - group.addTask { - let status = try await monitorProcessTermination( - forProcessWithIdentifier: process.processIdentifier) - return .monitorChildProcess(status) - } - group.addTask { - do { - let result = try await body(process) - try await self.cleanup( - process: process, - childSide: false, - parentSide: true, - attemptToTerminateSubProcess: false - ) - return .workBody(result) - } catch { - try await self.cleanup( - process: process, - childSide: false, - parentSide: true, - attemptToTerminateSubProcess: false - ) - throw error - } - } - - var result: R! - var terminationStatus: TerminationStatus! - while let state = try await group.next() { - switch state { - case .monitorChildProcess(let status): - terminationStatus = status - case .workBody(let workResult): - result = workResult - } - } - return ExecutionResult(terminationStatus: terminationStatus, value: result) - } - } onCancel: { - // Attempt to terminate the child process - // Since the task has already been cancelled, - // this is the best we can do - try? await self.cleanup( - process: process, - childSide: true, - parentSide: true, - attemptToTerminateSubProcess: true - ) - } - } - } -} - -extension Subprocess.Configuration : CustomStringConvertible, CustomDebugStringConvertible { - public var description: String { - return """ -Subprocess.Configuration( - executable: \(self.executable.description), - arguments: \(self.arguments.description), - environment: \(self.environment.description), - workingDirectory: \(self.workingDirectory), - platformOptions: \(self.platformOptions.description(withIndent: 1)) -) -""" - } - - public var debugDescription: String { - return """ -Subprocess.Configuration( - executable: \(self.executable.debugDescription), - arguments: \(self.arguments.debugDescription), - environment: \(self.environment.debugDescription), - workingDirectory: \(self.workingDirectory), - platformOptions: \(self.platformOptions.description(withIndent: 1)) -) -""" - } -} - -// MARK: - Executable -extension Subprocess { - /// `Subprocess.Executable` defines how should the executable - /// be looked up for execution. - public struct Executable: Sendable, Hashable { - internal enum Configuration: Sendable, Hashable { - case executable(String) - case path(FilePath) - } - - internal let storage: Configuration - - private init(_config: Configuration) { - self.storage = _config - } - - /// Locate the executable by its name. - /// `Subprocess` will use `PATH` value to - /// determine the full path to the executable. - public static func named(_ executableName: String) -> Self { - return .init(_config: .executable(executableName)) - } - /// Locate the executable by its full path. - /// `Subprocess` will use this path directly. - public static func at(_ filePath: FilePath) -> Self { - return .init(_config: .path(filePath)) - } - /// Returns the full executable path given the environment value. - public func resolveExecutablePath(in environment: Environment) -> FilePath? { - if let path = self.resolveExecutablePath(withPathValue: environment.pathValue()) { - return FilePath(path) - } - return nil - } - } -} - -extension Subprocess.Executable : CustomStringConvertible, CustomDebugStringConvertible { - public var description: String { - switch storage { - case .executable(let executableName): - return executableName - case .path(let filePath): - return filePath.string - } - } - - public var debugDescription: String { - switch storage { - case .executable(let string): - return "executable(\(string))" - case .path(let filePath): - return "path(\(filePath.string))" - } - } -} - -// MARK: - Arguments -extension Subprocess { - /// A collection of arguments to pass to the subprocess. - public struct Arguments: Sendable, ExpressibleByArrayLiteral, Hashable { - public typealias ArrayLiteralElement = String - - internal let storage: [StringOrRawBytes] - internal let executablePathOverride: StringOrRawBytes? - - /// Create an Arguments object using the given literal values - public init(arrayLiteral elements: String...) { - self.storage = elements.map { .string($0) } - self.executablePathOverride = nil - } - /// Create an Arguments object using the given array - public init(_ array: [String]) { - self.storage = array.map { .string($0) } - self.executablePathOverride = nil - } - -#if !os(Windows) // Windows does NOT support arg0 override - /// Create an `Argument` object using the given values, but - /// override the first Argument value to `executablePathOverride`. - /// If `executablePathOverride` is nil, - /// `Arguments` will automatically use the executable path - /// as the first argument. - /// - Parameters: - /// - executablePathOverride: the value to override the first argument. - /// - remainingValues: the rest of the argument value - public init(executablePathOverride: String?, remainingValues: [String]) { - self.storage = remainingValues.map { .string($0) } - if let executablePathOverride = executablePathOverride { - self.executablePathOverride = .string(executablePathOverride) - } else { - self.executablePathOverride = nil - } - } - - /// Create an `Argument` object using the given values, but - /// override the first Argument value to `executablePathOverride`. - /// If `executablePathOverride` is nil, - /// `Arguments` will automatically use the executable path - /// as the first argument. - /// - Parameters: - /// - executablePathOverride: the value to override the first argument. - /// - remainingValues: the rest of the argument value - public init(executablePathOverride: Data?, remainingValues: [Data]) { - self.storage = remainingValues.map { .rawBytes($0.toArray()) } - if let override = executablePathOverride { - self.executablePathOverride = .rawBytes(override.toArray()) - } else { - self.executablePathOverride = nil - } - } - - public init(_ array: [Data]) { - self.storage = array.map { .rawBytes($0.toArray()) } - self.executablePathOverride = nil - } -#endif - } -} - -extension Subprocess.Arguments : CustomStringConvertible, CustomDebugStringConvertible { - public var description: String { - var result: [String] = self.storage.map(\.description) - - if let override = self.executablePathOverride { - result.insert("override\(override.description)", at: 0) - } - return result.description - } - - public var debugDescription: String { return self.description } -} - -// MARK: - Environment -extension Subprocess { - /// A set of environment variables to use when executing the subprocess. - public struct Environment: Sendable, Hashable { - internal enum Configuration: Sendable, Hashable { - case inherit([StringOrRawBytes : StringOrRawBytes]) - case custom([StringOrRawBytes : StringOrRawBytes]) - } - - internal let config: Configuration - - init(config: Configuration) { - self.config = config - } - /// Child process should inherit the same environment - /// values from its parent process. - public static var inherit: Self { - return .init(config: .inherit([:])) - } - /// Override the provided `newValue` in the existing `Environment` - public func updating(_ newValue: [String : String]) -> Self { - return .init(config: .inherit(newValue.wrapToStringOrRawBytes())) - } - /// Use custom environment variables - public static func custom(_ newValue: [String : String]) -> Self { - return .init(config: .custom(newValue.wrapToStringOrRawBytes())) - } - -#if !os(Windows) - /// Override the provided `newValue` in the existing `Environment` - public func updating(_ newValue: [Data : Data]) -> Self { - return .init(config: .inherit(newValue.wrapToStringOrRawBytes())) - } - /// Use custom environment variables - public static func custom(_ newValue: [Data : Data]) -> Self { - return .init(config: .custom(newValue.wrapToStringOrRawBytes())) - } -#endif - } -} - -extension Subprocess.Environment : CustomStringConvertible, CustomDebugStringConvertible { - public var description: String { - switch self.config { - case .custom(let customDictionary): - return customDictionary.dictionaryDescription - case .inherit(let updateValue): - return "Inherting current environment with updates: \(updateValue.dictionaryDescription)" - } - } - - public var debugDescription: String { - return self.description - } -} - -fileprivate extension Dictionary where Key == String, Value == String { - func wrapToStringOrRawBytes() -> [Subprocess.StringOrRawBytes : Subprocess.StringOrRawBytes] { - var result = Dictionary< - Subprocess.StringOrRawBytes, - Subprocess.StringOrRawBytes - >(minimumCapacity: self.count) - for (key, value) in self { - result[.string(key)] = .string(value) - } - return result - } -} - -fileprivate extension Dictionary where Key == Data, Value == Data { - func wrapToStringOrRawBytes() -> [Subprocess.StringOrRawBytes : Subprocess.StringOrRawBytes] { - var result = Dictionary< - Subprocess.StringOrRawBytes, - Subprocess.StringOrRawBytes - >(minimumCapacity: self.count) - for (key, value) in self { - result[.rawBytes(key.toArray())] = .rawBytes(value.toArray()) - } - return result - } -} - -fileprivate extension Dictionary where Key == Subprocess.StringOrRawBytes, Value == Subprocess.StringOrRawBytes { - var dictionaryDescription: String { - var result = "[\n" - for (key, value) in self { - result += "\t\(key.description) : \(value.description),\n" - } - result += "]" - return result - } -} - -fileprivate extension Data { - func toArray() -> [T] { - return self.withUnsafeBytes { ptr in - return Array(ptr.bindMemory(to: T.self)) - } - } -} - -// MARK: - TerminationStatus -extension Subprocess { - /// An exit status of a subprocess. - @frozen - public enum TerminationStatus: Sendable, Hashable, Codable { - #if canImport(WinSDK) - public typealias Code = DWORD - #else - public typealias Code = CInt - #endif - - /// The subprocess was existed with the given code - case exited(Code) - /// The subprocess was signalled with given exception value - case unhandledException(Code) - /// Whether the current TerminationStatus is successful. - public var isSuccess: Bool { - switch self { - case .exited(let exitCode): - return exitCode == 0 - case .unhandledException(_): - return false - } - } - } -} - -extension Subprocess.TerminationStatus : CustomStringConvertible, CustomDebugStringConvertible { - public var description: String { - switch self { - case .exited(let code): - return "exited(\(code))" - case .unhandledException(let code): - return "unhandledException(\(code))" - } - } - - public var debugDescription: String { - return self.description - } -} - -// MARK: - Internal -extension Subprocess { - internal enum StringOrRawBytes: Sendable, Hashable { - case string(String) - case rawBytes([CChar]) - - // Return value needs to be deallocated manually by callee - func createRawBytes() -> UnsafeMutablePointer { - switch self { - case .string(let string): - return strdup(string) - case .rawBytes(let rawBytes): - return strdup(rawBytes) - } - } - - var stringValue: String? { - switch self { - case .string(let string): - return string - case .rawBytes(let rawBytes): - return String(validatingUTF8: rawBytes) - } - } - - var description: String { - switch self { - case .string(let string): - return string - case .rawBytes(let bytes): - return bytes.description - } - } - - var count: Int { - switch self { - case .string(let string): - return string.count - case .rawBytes(let rawBytes): - return strnlen(rawBytes, Int.max) - } - } - - func hash(into hasher: inout Hasher) { - // If Raw bytes is valid UTF8, hash it as so - switch self { - case .string(let string): - hasher.combine(string) - case .rawBytes(let bytes): - if let stringValue = self.stringValue { - hasher.combine(stringValue) - } else { - hasher.combine(bytes) - } - } - } - } -} - -extension FilePath { - static var currentWorkingDirectory: Self { - let path = getcwd(nil, 0)! - defer { free(path) } - return .init(String(cString: path)) - } -} - -extension Optional where Wrapped : Collection { - func withOptionalUnsafeBufferPointer(_ body: ((UnsafeBufferPointer)?) throws -> R) rethrows -> R { - switch self { - case .some(let wrapped): - guard let array: Array = wrapped as? Array else { - return try body(nil) - } - return try array.withUnsafeBufferPointer { ptr in - return try body(ptr) - } - case .none: - return try body(nil) - } - } -} - -extension Optional where Wrapped == String { - func withOptionalCString(_ body: ((UnsafePointer)?) throws -> R) rethrows -> R { - switch self { - case .none: - return try body(nil) - case .some(let wrapped): - return try wrapped.withCString { - return try body($0) - } - } - } - - var stringValue: String { - return self ?? "nil" - } -} - -// MARK: - Stubs for the one from Foundation -public enum QualityOfService: Int, Sendable { - case userInteractive = 0x21 - case userInitiated = 0x19 - case utility = 0x11 - case background = 0x09 - case `default` = -1 -} - -internal func withAsyncTaskCancellationHandler( - _ body: sending @escaping () async throws -> R, - onCancel handler: sending @escaping () async -> Void -) async rethrows -> R { - return try await withThrowingTaskGroup( - of: R?.self, - returning: R.self - ) { group in - group.addTask { - return try await body() - } - group.addTask { - // wait until cancelled - do { while true { try await Task.sleep(nanoseconds: 1_000_000_000) } } catch {} - // Run task cancel handler - await handler() - return nil - } - - while let result = try await group.next() { - if let result = result { - // As soon as the body finishes, cancel the group - group.cancelAll() - return result - } - } - fatalError("Unreachable") - } -} - diff --git a/Sources/_Subprocess/Subprocess+IO.swift b/Sources/_Subprocess/Subprocess+IO.swift deleted file mode 100644 index 1b43e4210..000000000 --- a/Sources/_Subprocess/Subprocess+IO.swift +++ /dev/null @@ -1,437 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift.org project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift.org project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -#if canImport(FoundationEssentials) -import FoundationEssentials -#elseif canImport(Foundation) -import Foundation -#endif - -import Dispatch -import SystemPackage - -// Naive Mutex so we don't have to update the macOS dependency -final class _Mutex: Sendable { - let lock = Lock() - - var value: Value - - init(_ value: Value) { - self.value = value - } - - func withLock(_ body: (inout Value) throws -> R) rethrows -> R { - self.lock.lock() - defer { self.lock.unlock() } - return try body(&self.value) - } -} - -// MARK: - Input -extension Subprocess { - /// `InputMethod` defines how should the standard input - /// of the subprocess receive inputs. - public struct InputMethod: Sendable, Hashable { - internal enum Storage: Sendable, Hashable { - case noInput - case fileDescriptor(FileDescriptor, Bool) - } - - internal let method: Storage - - internal init(method: Storage) { - self.method = method - } - - internal func createExecutionInput() throws -> ExecutionInput { - switch self.method { - case .noInput: - let devnull: FileDescriptor = try .openDevNull(withAcessMode: .readOnly) - return .init(storage: .noInput(devnull)) - case .fileDescriptor(let fileDescriptor, let closeWhenDone): - return .init(storage: .fileDescriptor(fileDescriptor, closeWhenDone)) - } - } - - /// Subprocess should read no input. This option is equivalent - /// to bind the stanard input to `/dev/null`. - public static var noInput: Self { - return .init(method: .noInput) - } - - /// Subprocess should read input from a given file descriptor. - /// - Parameters: - /// - fd: the file descriptor to read from - /// - closeAfterSpawningProcess: whether the file descriptor - /// should be automatically closed after subprocess is spawned. - public static func readFrom( - _ fd: FileDescriptor, - closeAfterSpawningProcess: Bool - ) -> Self { - return .init(method: .fileDescriptor(fd, closeAfterSpawningProcess)) - } - } -} - -extension Subprocess { - /// `CollectedOutputMethod` defines how should Subprocess collect - /// output from child process' standard output and standard error - public struct CollectedOutputMethod: Sendable, Hashable { - internal enum Storage: Sendable, Hashable { - case discarded - case fileDescriptor(FileDescriptor, Bool) - case collected(Int) - } - - internal let method: Storage - - internal init(method: Storage) { - self.method = method - } - - /// Subprocess shold dicard the child process output. - /// This option is equivalent to binding the child process - /// output to `/dev/null`. - public static var discard: Self { - return .init(method: .discarded) - } - /// Subprocess should write the child process output - /// to the file descriptor specified. - /// - Parameters: - /// - fd: the file descriptor to write to - /// - closeAfterSpawningProcess: whether to close the - /// file descriptor once the process is spawned. - public static func writeTo(_ fd: FileDescriptor, closeAfterSpawningProcess: Bool) -> Self { - return .init(method: .fileDescriptor(fd, closeAfterSpawningProcess)) - } - /// Subprocess should collect the child process output - /// as `Data` with the given limit in bytes. The default - /// limit is 128kb. - public static func collect(upTo limit: Int = 128 * 1024) -> Self { - return .init(method: .collected(limit)) - } - - internal func createExecutionOutput() throws -> ExecutionOutput { - switch self.method { - case .discarded: - // Bind to /dev/null - let devnull: FileDescriptor = try .openDevNull(withAcessMode: .writeOnly) - return .init(storage: .discarded(devnull)) - case .fileDescriptor(let fileDescriptor, let closeWhenDone): - return .init(storage: .fileDescriptor(fileDescriptor, closeWhenDone)) - case .collected(let limit): - let (readFd, writeFd) = try FileDescriptor.pipe() - return .init(storage: .collected(limit, readFd, writeFd)) - } - } - } - - /// `CollectedOutputMethod` defines how should Subprocess redirect - /// output from child process' standard output and standard error. - public struct RedirectedOutputMethod: Sendable, Hashable { - typealias Storage = CollectedOutputMethod.Storage - - internal let method: Storage - - internal init(method: Storage) { - self.method = method - } - - /// Subprocess shold dicard the child process output. - /// This option is equivalent to binding the child process - /// output to `/dev/null`. - public static var discard: Self { - return .init(method: .discarded) - } - /// Subprocess should redirect the child process output - /// to `Subprocess.standardOutput` or `Subprocess.standardError` - /// so they can be consumed as an AsyncSequence - public static var redirectToSequence: Self { - return .init(method: .collected(128 * 1024)) - } - /// Subprocess shold write the child process output - /// to the file descriptor specified. - /// - Parameters: - /// - fd: the file descriptor to write to - /// - closeAfterSpawningProcess: whether to close the - /// file descriptor once the process is spawned. - public static func writeTo( - _ fd: FileDescriptor, - closeAfterSpawningProcess: Bool - ) -> Self { - return .init(method: .fileDescriptor(fd, closeAfterSpawningProcess)) - } - - internal func createExecutionOutput() throws -> ExecutionOutput { - switch self.method { - case .discarded: - // Bind to /dev/null - let devnull: FileDescriptor = try .openDevNull(withAcessMode: .writeOnly) - return .init(storage: .discarded(devnull)) - case .fileDescriptor(let fileDescriptor, let closeWhenDone): - return .init(storage: .fileDescriptor(fileDescriptor, closeWhenDone)) - case .collected(let limit): - let (readFd, writeFd) = try FileDescriptor.pipe() - return .init(storage: .collected(limit, readFd, writeFd)) - } - } - } -} - -// MARK: - Execution IO -extension Subprocess { - internal final class ExecutionInput: Sendable, Hashable { - - - internal enum Storage: Sendable, Hashable { - case noInput(FileDescriptor?) - case customWrite(FileDescriptor?, FileDescriptor?) - case fileDescriptor(FileDescriptor?, Bool) - } - - let storage: _Mutex - - internal init(storage: Storage) { - self.storage = .init(storage) - } - - internal func getReadFileDescriptor() -> FileDescriptor? { - return self.storage.withLock { - switch $0 { - case .noInput(let readFd): - return readFd - case .customWrite(let readFd, _): - return readFd - case .fileDescriptor(let readFd, _): - return readFd - } - } - } - - internal func getWriteFileDescriptor() -> FileDescriptor? { - return self.storage.withLock { - switch $0 { - case .noInput(_), .fileDescriptor(_, _): - return nil - case .customWrite(_, let writeFd): - return writeFd - } - } - } - - internal func closeChildSide() throws { - try self.storage.withLock { - switch $0 { - case .noInput(let devnull): - try devnull?.close() - $0 = .noInput(nil) - case .customWrite(let readFd, let writeFd): - try readFd?.close() - $0 = .customWrite(nil, writeFd) - case .fileDescriptor(let fd, let closeWhenDone): - // User passed in fd - if closeWhenDone { - try fd?.close() - $0 = .fileDescriptor(nil, closeWhenDone) - } - } - } - } - - internal func closeParentSide() throws { - try self.storage.withLock { - switch $0 { - case .noInput(_), .fileDescriptor(_, _): - break - case .customWrite(let readFd, let writeFd): - // The parent fd should have been closed - // in the `body` when writer.finish() is called - // But in case it isn't call it agian - try writeFd?.close() - $0 = .customWrite(readFd, nil) - } - } - } - - internal func closeAll() throws { - try self.storage.withLock { - switch $0 { - case .noInput(let readFd): - try readFd?.close() - $0 = .noInput(nil) - case .customWrite(let readFd, let writeFd): - var readFdCloseError: Error? - var writeFdCloseError: Error? - do { - try readFd?.close() - } catch { - readFdCloseError = error - } - do { - try writeFd?.close() - } catch { - writeFdCloseError = error - } - $0 = .customWrite(nil, nil) - if let readFdCloseError { - throw readFdCloseError - } - if let writeFdCloseError { - throw writeFdCloseError - } - case .fileDescriptor(let fd, let closeWhenDone): - if closeWhenDone { - try fd?.close() - $0 = .fileDescriptor(nil, closeWhenDone) - } - } - } - } - - public func hash(into hasher: inout Hasher) { - self.storage.withLock { - hasher.combine($0) - } - } - - public static func == ( - lhs: Subprocess.ExecutionInput, - rhs: Subprocess.ExecutionInput - ) -> Bool { - return lhs.storage.withLock { lhsStorage in - rhs.storage.withLock { rhsStorage in - return lhsStorage == rhsStorage - } - } - } - } - - internal final class ExecutionOutput: Sendable { - internal enum Storage: Sendable { - case discarded(FileDescriptor?) - case fileDescriptor(FileDescriptor?, Bool) - case collected(Int, FileDescriptor?, FileDescriptor?) - } - - private let storage: _Mutex - - internal init(storage: Storage) { - self.storage = .init(storage) - } - - internal func getWriteFileDescriptor() -> FileDescriptor? { - return self.storage.withLock { - switch $0 { - case .discarded(let writeFd): - return writeFd - case .fileDescriptor(let writeFd, _): - return writeFd - case .collected(_, _, let writeFd): - return writeFd - } - } - } - - internal func getReadFileDescriptor() -> FileDescriptor? { - return self.storage.withLock { - switch $0 { - case .discarded(_), .fileDescriptor(_, _): - return nil - case .collected(_, let readFd, _): - return readFd - } - } - } - - internal func consumeCollectedFileDescriptor() -> (limit: Int, fd: FileDescriptor?)? { - return self.storage.withLock { - switch $0 { - case .discarded(_), .fileDescriptor(_, _): - // The output has been written somewhere else - return nil - case .collected(let limit, let readFd, let writeFd): - $0 = .collected(limit, nil, writeFd) - return (limit, readFd) - } - } - } - - internal func closeChildSide() throws { - try self.storage.withLock { - switch $0 { - case .discarded(let writeFd): - try writeFd?.close() - $0 = .discarded(nil) - case .fileDescriptor(let fd, let closeWhenDone): - // User passed fd - if closeWhenDone { - try fd?.close() - $0 = .fileDescriptor(nil, closeWhenDone) - } - case .collected(let limit, let readFd, let writeFd): - try writeFd?.close() - $0 = .collected(limit, readFd, nil) - } - } - } - - internal func closeParentSide() throws { - try self.storage.withLock { - switch $0 { - case .discarded(_), .fileDescriptor(_, _): - break - case .collected(let limit, let readFd, let writeFd): - try readFd?.close() - $0 = .collected(limit, nil, writeFd) - } - } - } - - internal func closeAll() throws { - try self.storage.withLock { - switch $0 { - case .discarded(let writeFd): - try writeFd?.close() - $0 = .discarded(nil) - case .fileDescriptor(let fd, let closeWhenDone): - if closeWhenDone { - try fd?.close() - $0 = .fileDescriptor(nil, closeWhenDone) - } - case .collected(let limit, let readFd, let writeFd): - var readFdCloseError: Error? - var writeFdCloseError: Error? - do { - try readFd?.close() - } catch { - readFdCloseError = error - } - do { - try writeFd?.close() - } catch { - writeFdCloseError = error - } - $0 = .collected(limit, nil, nil) - if let readFdCloseError { - throw readFdCloseError - } - if let writeFdCloseError { - throw writeFdCloseError - } - } - } - } - } -} - diff --git a/Sources/_Subprocess/Subprocess+Teardown.swift b/Sources/_Subprocess/Subprocess+Teardown.swift deleted file mode 100644 index 8b9af32dd..000000000 --- a/Sources/_Subprocess/Subprocess+Teardown.swift +++ /dev/null @@ -1,125 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift.org project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift.org project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -#if canImport(Darwin) || canImport(Glibc) - -#if canImport(Darwin) -import Darwin -#elseif canImport(Glibc) -import Glibc -#endif - -#if canImport(FoundationEssentials) -import FoundationEssentials -#elseif canImport(Foundation) -import Foundation -#endif - -extension Subprocess { - /// A step in the graceful shutdown teardown sequence. - /// It consists of a signal to send to the child process and the - /// number of nanoseconds allowed for the child process to exit - /// before proceeding to the next step. - public struct TeardownStep: Sendable, Hashable { - internal enum Storage: Sendable, Hashable { - case sendSignal(Signal, allowedNanoseconds: UInt64) - case kill - } - var storage: Storage - - /// Sends `signal` to the process and provides `allowedNanoSecondsToExit` - /// nanoseconds for the process to exit before proceeding to the next step. - /// The final step in the sequence will always send a `.kill` signal. - public static func sendSignal( - _ signal: Signal, - allowedNanoSecondsToExit: UInt64 - ) -> Self { - return Self( - storage: .sendSignal( - signal, - allowedNanoseconds: allowedNanoSecondsToExit - ) - ) - } - } -} - -extension Subprocess { - internal func runTeardownSequence(_ sequence: [TeardownStep]) async { - // First insert the `.kill` step - let finalSequence = sequence + [TeardownStep(storage: .kill)] - for step in finalSequence { - enum TeardownStepCompletion { - case processHasExited - case processStillAlive - case killedTheProcess - } - let stepCompletion: TeardownStepCompletion - - guard self.isAlive() else { - return - } - - switch step.storage { - case .sendSignal(let signal, let allowedNanoseconds): - stepCompletion = await withTaskGroup(of: TeardownStepCompletion.self) { group in - group.addTask { - do { - try await Task.sleep(nanoseconds: allowedNanoseconds) - return .processStillAlive - } catch { - // teardown(using:) cancells this task - // when process has exited - return .processHasExited - } - } - try? self.send(signal, toProcessGroup: false) - return await group.next()! - } - case .kill: - try? self.send(.kill, toProcessGroup: false) - stepCompletion = .killedTheProcess - } - - switch stepCompletion { - case .killedTheProcess, .processHasExited: - return - case .processStillAlive: - // Continue to next step - break - } - } - } -} - -extension Subprocess { - private func isAlive() -> Bool { - return kill(self.processIdentifier.value, 0) == 0 - } -} - -func withUncancelledTask( - returning: R.Type = R.self, - _ body: @Sendable @escaping () async -> R -) async -> R { - // This looks unstructured but it isn't, please note that we `await` `.value` of this task. - // The reason we need this separate `Task` is that in general, we cannot assume that code performs to our - // expectations if the task we run it on is already cancelled. However, in some cases we need the code to - // run regardless -- even if our task is already cancelled. Therefore, we create a new, uncancelled task here. - await Task { - await body() - }.value -} - -#endif // canImport(Darwin) || canImport(Glibc) diff --git a/Sources/_Subprocess/Subprocess.swift b/Sources/_Subprocess/Subprocess.swift deleted file mode 100644 index 35f4a2c21..000000000 --- a/Sources/_Subprocess/Subprocess.swift +++ /dev/null @@ -1,315 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift.org project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift.org project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import SystemPackage - -#if canImport(FoundationEssentials) -import FoundationEssentials -#elseif canImport(Foundation) -import Foundation -#endif - -/// An object that represents a subprocess of the current process. -/// -/// Using `Subprocess`, your program can run another program as a subprocess -/// and can monitor that program’s execution. A `Subprocess` object creates a -/// **separate executable** entity; it’s different from `Thread` because it doesn’t -/// share memory space with the process that creates it. -public struct Subprocess: Sendable { - /// The process identifier of the current subprocess - public let processIdentifier: ProcessIdentifier - - internal let executionInput: ExecutionInput - internal let executionOutput: ExecutionOutput - internal let executionError: ExecutionOutput -#if os(Windows) - internal let consoleBehavior: PlatformOptions.ConsoleBehavior -#endif - - /// The standard output of the subprocess. - /// Accessing this property will **fatalError** if - /// - `.output` wasn't set to `.redirectToSequence` when the subprocess was spawned; - /// - This property was accessed multiple times. Subprocess communicates with - /// parent process via pipe under the hood and each pipe can only be consumed ones. - public var standardOutput: some _AsyncSequence { - guard let (_, fd) = self.executionOutput - .consumeCollectedFileDescriptor() else { - fatalError("The standard output was not redirected") - } - guard let fd = fd else { - fatalError("The standard output has already been closed") - } - return AsyncDataSequence(fileDescriptor: fd) - } - - /// The standard error of the subprocess. - /// Accessing this property will **fatalError** if - /// - `.error` wasn't set to `.redirectToSequence` when the subprocess was spawned; - /// - This property was accessed multiple times. Subprocess communicates with - /// parent process via pipe under the hood and each pipe can only be consumed ones. - public var standardError: some _AsyncSequence { - guard let (_, fd) = self.executionError - .consumeCollectedFileDescriptor() else { - fatalError("The standard error was not redirected") - } - guard let fd = fd else { - fatalError("The standard error has already been closed") - } - return AsyncDataSequence(fileDescriptor: fd) - } -} - -// MARK: - Teardown -#if canImport(Darwin) || canImport(Glibc) -extension Subprocess { - /// Performs a sequence of teardown steps on the Subprocess. - /// Teardown sequence always ends with a `.kill` signal - /// - Parameter sequence: The steps to perform. - public func teardown(using sequence: [TeardownStep]) async { - await withUncancelledTask { - await self.runTeardownSequence(sequence) - } - } -} -#endif - -// MARK: - StandardInputWriter -extension Subprocess { - /// A writer that writes to the standard input of the subprocess. - public struct StandardInputWriter { - - private let input: ExecutionInput - - init(input: ExecutionInput) { - self.input = input - } - - /// Write a sequence of UInt8 to the standard input of the subprocess. - /// - Parameter sequence: The sequence of bytes to write. - public func write(_ sequence: S) async throws where S : Sequence, S.Element == UInt8 { - guard let fd: FileDescriptor = self.input.getWriteFileDescriptor() else { - fatalError("Attempting to write to a file descriptor that's already closed") - } - try await fd.write(sequence) - } - - /// Write a sequence of CChar to the standard input of the subprocess. - /// - Parameter sequence: The sequence of bytes to write. - public func write(_ sequence: S) async throws where S : Sequence, S.Element == CChar { - try await self.write(sequence.map { UInt8($0) }) - } - - /// Write a AsyncSequence of CChar to the standard input of the subprocess. - /// - Parameter sequence: The sequence of bytes to write. - public func write(_ asyncSequence: S) async throws where S.Element == CChar { - let sequence = try await Array(asyncSequence).map { UInt8($0) } - try await self.write(sequence) - } - - /// Write a AsyncSequence of UInt8 to the standard input of the subprocess. - /// - Parameter sequence: The sequence of bytes to write. - public func write(_ asyncSequence: S) async throws where S.Element == UInt8 { - let sequence = try await Array(asyncSequence) - try await self.write(sequence) - } - - /// Signal all writes are finished - public func finish() async throws { - try self.input.closeParentSide() - } - } -} - -@available(macOS, unavailable) -@available(iOS, unavailable) -@available(tvOS, unavailable) -@available(watchOS, unavailable) -@available(*, unavailable) -extension Subprocess.StandardInputWriter : Sendable {} - -// MARK: - Result -extension Subprocess { - /// A simple wrapper around the generic result returned by the - /// `run` closures with the corresponding `TerminationStatus` - /// of the child process. - public struct ExecutionResult: Sendable { - /// The termination status of the child process - public let terminationStatus: TerminationStatus - /// The result returned by the closure passed to `.run` methods - public let value: T - - internal init(terminationStatus: TerminationStatus, value: T) { - self.terminationStatus = terminationStatus - self.value = value - } - } - - /// The result of a subprocess execution with its collected - /// standard output and standard error. - public struct CollectedResult: Sendable, Hashable, Codable { - /// The process identifier for the executed subprocess - public let processIdentifier: ProcessIdentifier - /// The termination status of the executed subprocess - public let terminationStatus: TerminationStatus - private let _standardOutput: Data? - private let _standardError: Data? - - /// The collected standard output value for the subprocess. - /// Accessing this property will *fatalError* if the - /// corresponding `CollectedOutputMethod` is not set to - /// `.collect` or `.collect(upTo:)` - public var standardOutput: Data { - guard let output = self._standardOutput else { - fatalError("standardOutput is only available if the Subprocess was ran with .collect as output") - } - return output - } - /// The collected standard error value for the subprocess. - /// Accessing this property will *fatalError* if the - /// corresponding `CollectedOutputMethod` is not set to - /// `.collect` or `.collect(upTo:)` - public var standardError: Data { - guard let output = self._standardError else { - fatalError("standardError is only available if the Subprocess was ran with .collect as error ") - } - return output - } - - internal init( - processIdentifier: ProcessIdentifier, - terminationStatus: TerminationStatus, - standardOutput: Data?, - standardError: Data?) { - self.processIdentifier = processIdentifier - self.terminationStatus = terminationStatus - self._standardOutput = standardOutput - self._standardError = standardError - } - } -} - -extension Subprocess.ExecutionResult: Equatable where T : Equatable {} - -extension Subprocess.ExecutionResult: Hashable where T : Hashable {} - -extension Subprocess.ExecutionResult: Codable where T : Codable {} - -extension Subprocess.ExecutionResult: CustomStringConvertible where T : CustomStringConvertible { - public var description: String { - return """ -Subprocess.ExecutionResult( - terminationStatus: \(self.terminationStatus.description), - value: \(self.value.description) -) -""" - } -} - -extension Subprocess.ExecutionResult: CustomDebugStringConvertible where T : CustomDebugStringConvertible { - public var debugDescription: String { - return """ -Subprocess.ExecutionResult( - terminationStatus: \(self.terminationStatus.debugDescription), - value: \(self.value.debugDescription) -) -""" - } -} - -extension Subprocess.CollectedResult : CustomStringConvertible, CustomDebugStringConvertible { - public var description: String { - return """ -Subprocess.CollectedResult( - processIdentifier: \(self.processIdentifier.description), - terminationStatus: \(self.terminationStatus.description), - standardOutput: \(self._standardOutput?.description ?? "not captured"), - standardError: \(self._standardError?.description ?? "not captured") -) -""" - } - - public var debugDescription: String { - return """ -Subprocess.CollectedResult( - processIdentifier: \(self.processIdentifier.debugDescription), - terminationStatus: \(self.terminationStatus.debugDescription), - standardOutput: \(self._standardOutput?.debugDescription ?? "not captured"), - standardError: \(self._standardError?.debugDescription ?? "not captured") -) -""" - } -} - -// MARK: - Internal -extension Subprocess { - internal enum OutputCapturingState { - case standardOutputCaptured(Data?) - case standardErrorCaptured(Data?) - } - - internal typealias CapturedIOs = (standardOutput: Data?, standardError: Data?) - - private func capture(fileDescriptor: FileDescriptor, maxLength: Int) async throws -> Data { - return try await fileDescriptor.readUntilEOF(upToLength: maxLength) - } - - internal func captureStandardOutput() async throws -> Data? { - guard let (limit, readFd) = self.executionOutput - .consumeCollectedFileDescriptor(), - let readFd = readFd else { - return nil - } - defer { - try? readFd.close() - } - return try await self.capture(fileDescriptor: readFd, maxLength: limit) - } - - internal func captureStandardError() async throws -> Data? { - guard let (limit, readFd) = self.executionError - .consumeCollectedFileDescriptor(), - let readFd = readFd else { - return nil - } - defer { - try? readFd.close() - } - return try await self.capture(fileDescriptor: readFd, maxLength: limit) - } - - internal func captureIOs() async throws -> CapturedIOs { - return try await withThrowingTaskGroup(of: OutputCapturingState.self) { group in - group.addTask { - let stdout = try await self.captureStandardOutput() - return .standardOutputCaptured(stdout) - } - group.addTask { - let stderr = try await self.captureStandardError() - return .standardErrorCaptured(stderr) - } - - var stdout: Data? - var stderror: Data? - while let state = try await group.next() { - switch state { - case .standardOutputCaptured(let output): - stdout = output - case .standardErrorCaptured(let error): - stderror = error - } - } - return (standardOutput: stdout, standardError: stderror) - } - } -} diff --git a/Sources/_Subprocess/SubprocessFoundation/Input+Foundation.swift b/Sources/_Subprocess/SubprocessFoundation/Input+Foundation.swift new file mode 100644 index 000000000..3ec421737 --- /dev/null +++ b/Sources/_Subprocess/SubprocessFoundation/Input+Foundation.swift @@ -0,0 +1,179 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +#if SubprocessFoundation + +#if canImport(Darwin) +// On Darwin always prefer system Foundation +import Foundation +#else +// On other platforms prefer FoundationEssentials +import FoundationEssentials +#endif // canImport(Darwin) + +#if canImport(System) +import System +#else +@preconcurrency import SystemPackage +#endif + +internal import Dispatch + +/// A concrete `Input` type for subprocesses that reads input +/// from a given `Data`. +public struct DataInput: InputProtocol { + private let data: Data + + public func write(with writer: StandardInputWriter) async throws { + _ = try await writer.write(self.data) + } + + internal init(data: Data) { + self.data = data + } +} + +/// A concrete `Input` type for subprocesses that accepts input +/// from a specified sequence of `Data`. +public struct DataSequenceInput< + InputSequence: Sequence & Sendable +>: InputProtocol where InputSequence.Element == Data { + private let sequence: InputSequence + + public func write(with writer: StandardInputWriter) async throws { + var buffer = Data() + for chunk in self.sequence { + buffer.append(chunk) + } + _ = try await writer.write(buffer) + } + + internal init(underlying: InputSequence) { + self.sequence = underlying + } +} + +/// A concrete `Input` type for subprocesses that reads input +/// from a given async sequence of `Data`. +public struct DataAsyncSequenceInput< + InputSequence: AsyncSequence & Sendable +>: InputProtocol where InputSequence.Element == Data { + private let sequence: InputSequence + + private func writeChunk(_ chunk: Data, with writer: StandardInputWriter) async throws { + _ = try await writer.write(chunk) + } + + public func write(with writer: StandardInputWriter) async throws { + for try await chunk in self.sequence { + try await self.writeChunk(chunk, with: writer) + } + } + + internal init(underlying: InputSequence) { + self.sequence = underlying + } +} + +extension InputProtocol { + /// Create a Subprocess input from a `Data` + public static func data(_ data: Data) -> Self where Self == DataInput { + return DataInput(data: data) + } + + /// Create a Subprocess input from a `Sequence` of `Data`. + public static func sequence( + _ sequence: InputSequence + ) -> Self where Self == DataSequenceInput { + return .init(underlying: sequence) + } + + /// Create a Subprocess input from a `AsyncSequence` of `Data`. + public static func sequence( + _ asyncSequence: InputSequence + ) -> Self where Self == DataAsyncSequenceInput { + return .init(underlying: asyncSequence) + } +} + +extension StandardInputWriter { + /// Write a `Data` to the standard input of the subprocess. + /// - Parameter data: The sequence of bytes to write. + /// - Returns number of bytes written. + public func write( + _ data: Data + ) async throws -> Int { + return try await self.fileDescriptor.wrapped.write(data) + } + + /// Write a AsyncSequence of Data to the standard input of the subprocess. + /// - Parameter sequence: The sequence of bytes to write. + /// - Returns number of bytes written. + public func write( + _ asyncSequence: AsyncSendableSequence + ) async throws -> Int where AsyncSendableSequence.Element == Data { + var buffer = Data() + for try await data in asyncSequence { + buffer.append(data) + } + return try await self.write(buffer) + } +} + +extension FileDescriptor { + #if os(Windows) + internal func write( + _ data: Data + ) async throws -> Int { + try await withCheckedThrowingContinuation { continuation in + // TODO: Figure out a better way to asynchornously write + DispatchQueue.global(qos: .userInitiated).async { + data.withUnsafeBytes { + self.write($0) { writtenLength, error in + if let error = error { + continuation.resume(throwing: error) + } else { + continuation.resume(returning: writtenLength) + } + } + } + } + } + } + #else + internal func write( + _ data: Data + ) async throws -> Int { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + let dispatchData = data.withUnsafeBytes { + return DispatchData( + bytesNoCopy: $0, + deallocator: .custom( + nil, + { + // noop + } + ) + ) + } + self.write(dispatchData) { writtenLength, error in + if let error = error { + continuation.resume(throwing: error) + } else { + continuation.resume(returning: writtenLength) + } + } + } + } + #endif +} + +#endif // SubprocessFoundation diff --git a/Sources/_Subprocess/SubprocessFoundation/Output+Foundation.swift b/Sources/_Subprocess/SubprocessFoundation/Output+Foundation.swift new file mode 100644 index 000000000..fac0bb8f1 --- /dev/null +++ b/Sources/_Subprocess/SubprocessFoundation/Output+Foundation.swift @@ -0,0 +1,53 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +#if SubprocessFoundation + +#if canImport(Darwin) +// On Darwin always prefer system Foundation +import Foundation +#else +// On other platforms prefer FoundationEssentials +import FoundationEssentials +#endif + +/// A concrete `Output` type for subprocesses that collects output +/// from the subprocess as `Data`. This option must be used with +/// the `run()` method that returns a `CollectedResult` +public struct DataOutput: OutputProtocol { + public typealias OutputType = Data + public let maxSize: Int + + public func output(from buffer: some Sequence) throws -> Data { + return Data(buffer) + } + + internal init(limit: Int) { + self.maxSize = limit + } +} + +extension OutputProtocol where Self == DataOutput { + /// Create a `Subprocess` output that collects output as `Data` + /// up to 128kb. + public static var data: Self { + return .data(limit: 128 * 1024) + } + + /// Create a `Subprocess` output that collects output as `Data` + /// with given max number of bytes to collect. + public static func data(limit: Int) -> Self { + return .init(limit: limit) + } +} + + +#endif // SubprocessFoundation diff --git a/Sources/_Subprocess/SubprocessFoundation/Span+SubprocessFoundation.swift b/Sources/_Subprocess/SubprocessFoundation/Span+SubprocessFoundation.swift new file mode 100644 index 000000000..10697091e --- /dev/null +++ b/Sources/_Subprocess/SubprocessFoundation/Span+SubprocessFoundation.swift @@ -0,0 +1,76 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +#if SubprocessFoundation && SubprocessSpan + +#if canImport(Darwin) +// On Darwin always prefer system Foundation +import Foundation +#else +// On other platforms prefer FoundationEssentials +import FoundationEssentials +#endif // canImport(Darwin) + +internal import Dispatch + + +extension Data { + init(_ s: borrowing RawSpan) { + self = s.withUnsafeBytes { Data($0) } + } + + public var bytes: RawSpan { + // FIXME: For demo purpose only + let ptr = self.withUnsafeBytes { ptr in + return ptr + } + let span = RawSpan(_unsafeBytes: ptr) + return _overrideLifetime(of: span, to: self) + } +} + + +extension DataProtocol { + var bytes: RawSpan { + _read { + if self.regions.isEmpty { + let empty = UnsafeRawBufferPointer(start: nil, count: 0) + let span = RawSpan(_unsafeBytes: empty) + yield _overrideLifetime(of: span, to: self) + } else if self.regions.count == 1 { + // Easy case: there is only one region in the data + let ptr = self.regions.first!.withUnsafeBytes { ptr in + return ptr + } + let span = RawSpan(_unsafeBytes: ptr) + yield _overrideLifetime(of: span, to: self) + } else { + // This data contains discontiguous chunks. We have to + // copy and make a contiguous chunk + var contiguous: ContiguousArray? + for region in self.regions { + if contiguous != nil { + contiguous?.append(contentsOf: region) + } else { + contiguous = .init(region) + } + } + let ptr = contiguous!.withUnsafeBytes { ptr in + return ptr + } + let span = RawSpan(_unsafeBytes: ptr) + yield _overrideLifetime(of: span, to: self) + } + } + } +} + +#endif // SubprocessFoundation diff --git a/Sources/_Subprocess/Teardown.swift b/Sources/_Subprocess/Teardown.swift new file mode 100644 index 000000000..b2da85a5f --- /dev/null +++ b/Sources/_Subprocess/Teardown.swift @@ -0,0 +1,217 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import _SubprocessCShims + +#if canImport(Darwin) +import Darwin +#elseif canImport(Bionic) +import Bionic +#elseif canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#elseif canImport(WinSDK) +import WinSDK +#endif + +/// A step in the graceful shutdown teardown sequence. +/// It consists of an action to perform on the child process and the +/// duration allowed for the child process to exit before proceeding +/// to the next step. +public struct TeardownStep: Sendable, Hashable { + internal enum Storage: Sendable, Hashable { + #if !os(Windows) + case sendSignal(Signal, allowedDuration: Duration) + #endif + case gracefulShutDown(allowedDuration: Duration) + case kill + } + var storage: Storage + + #if !os(Windows) + /// Sends `signal` to the process and allows `allowedDurationToExit` + /// for the process to exit before proceeding to the next step. + /// The final step in the sequence will always send a `.kill` signal. + public static func send( + signal: Signal, + allowedDurationToNextStep: Duration + ) -> Self { + return Self( + storage: .sendSignal( + signal, + allowedDuration: allowedDurationToNextStep + ) + ) + } + #endif // !os(Windows) + + /// Attempt to perform a graceful shutdown and allows + /// `allowedDurationToNextStep` for the process to exit + /// before proceeding to the next step: + /// - On Unix: send `SIGTERM` + /// - On Windows: + /// 1. Attempt to send `VM_CLOSE` if the child process is a GUI process; + /// 2. Attempt to send `CTRL_C_EVENT` to console; + /// 3. Attempt to send `CTRL_BREAK_EVENT` to process group. + public static func gracefulShutDown( + allowedDurationToNextStep: Duration + ) -> Self { + return Self( + storage: .gracefulShutDown( + allowedDuration: allowedDurationToNextStep + ) + ) + } +} + +@available(macOS 15.0, *) // FIXME: manually added availability +extension Execution { + /// Performs a sequence of teardown steps on the Subprocess. + /// Teardown sequence always ends with a `.kill` signal + /// - Parameter sequence: The steps to perform. + public func teardown(using sequence: some Sequence & Sendable) async { + await withUncancelledTask { + await self.runTeardownSequence(sequence) + } + } +} + +internal enum TeardownStepCompletion { + case processHasExited + case processStillAlive + case killedTheProcess +} + +@available(macOS 15.0, *) // FIXME: manually added availability +extension Execution { + internal func gracefulShutDown( + allowedDurationToNextStep duration: Duration + ) async { + #if os(Windows) + guard + let processHandle = OpenProcess( + DWORD(PROCESS_QUERY_INFORMATION | SYNCHRONIZE), + false, + self.processIdentifier.value + ) + else { + // Nothing more we can do + return + } + defer { + CloseHandle(processHandle) + } + + // 1. Attempt to send WM_CLOSE to the main window + if _subprocess_windows_send_vm_close( + self.processIdentifier.value + ) { + try? await Task.sleep(for: duration) + } + + // 2. Attempt to attach to the console and send CTRL_C_EVENT + if AttachConsole(self.processIdentifier.value) { + // Disable Ctrl-C handling in this process + if SetConsoleCtrlHandler(nil, true) { + if GenerateConsoleCtrlEvent(DWORD(CTRL_C_EVENT), 0) { + // We successfully sent the event. wait for the process to exit + try? await Task.sleep(for: duration) + } + // Re-enable Ctrl-C handling + SetConsoleCtrlHandler(nil, false) + } + // Detach console + FreeConsole() + } + + // 3. Attempt to send CTRL_BREAK_EVENT to the process group + if GenerateConsoleCtrlEvent(DWORD(CTRL_BREAK_EVENT), self.processIdentifier.value) { + // Wait for process to exit + try? await Task.sleep(for: duration) + } + #else + // Send SIGTERM + try? self.send(signal: .terminate) + #endif + } + + internal func runTeardownSequence(_ sequence: some Sequence & Sendable) async { + // First insert the `.kill` step + let finalSequence = sequence + [TeardownStep(storage: .kill)] + for step in finalSequence { + let stepCompletion: TeardownStepCompletion + + switch step.storage { + case .gracefulShutDown(let allowedDuration): + stepCompletion = await withTaskGroup(of: TeardownStepCompletion.self) { group in + group.addTask { + do { + try await Task.sleep(for: allowedDuration) + return .processStillAlive + } catch { + // teardown(using:) cancells this task + // when process has exited + return .processHasExited + } + } + await self.gracefulShutDown(allowedDurationToNextStep: allowedDuration) + return await group.next()! + } + #if !os(Windows) + case .sendSignal(let signal, let allowedDuration): + stepCompletion = await withTaskGroup(of: TeardownStepCompletion.self) { group in + group.addTask { + do { + try await Task.sleep(for: allowedDuration) + return .processStillAlive + } catch { + // teardown(using:) cancells this task + // when process has exited + return .processHasExited + } + } + try? self.send(signal: signal) + return await group.next()! + } + #endif // !os(Windows) + case .kill: + #if os(Windows) + try? self.terminate(withExitCode: 0) + #else + try? self.send(signal: .kill) + #endif + stepCompletion = .killedTheProcess + } + + switch stepCompletion { + case .killedTheProcess, .processHasExited: + return + case .processStillAlive: + // Continue to next step + break + } + } + } +} + +func withUncancelledTask( + returning: Result.Type = Result.self, + _ body: @Sendable @escaping () async -> Result +) async -> Result { + // This looks unstructured but it isn't, please note that we `await` `.value` of this task. + // The reason we need this separate `Task` is that in general, we cannot assume that code performs to our + // expectations if the task we run it on is already cancelled. However, in some cases we need the code to + // run regardless -- even if our task is already cancelled. Therefore, we create a new, uncancelled task here. + await Task { + await body() + }.value +} diff --git a/Sources/_Subprocess/_nio_locks.swift b/Sources/_Subprocess/_nio_locks.swift deleted file mode 100644 index 49053d048..000000000 --- a/Sources/_Subprocess/_nio_locks.swift +++ /dev/null @@ -1,526 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftNIO open source project -// -// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftNIO project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -#if canImport(Darwin) -import Darwin -#elseif os(Windows) -import ucrt -import WinSDK -#elseif canImport(Glibc) -import Glibc -#elseif canImport(Musl) -import Musl -#else -#error("The concurrency lock module was unable to identify your C library.") -#endif - -/// A threading lock based on `libpthread` instead of `libdispatch`. -/// -/// This object provides a lock on top of a single `pthread_mutex_t`. This kind -/// of lock is safe to use with `libpthread`-based threading models, such as the -/// one used by NIO. On Windows, the lock is based on the substantially similar -/// `SRWLOCK` type. -@available(*, deprecated, renamed: "NIOLock") -public final class Lock { -#if os(Windows) - fileprivate let mutex: UnsafeMutablePointer = - UnsafeMutablePointer.allocate(capacity: 1) -#else - fileprivate let mutex: UnsafeMutablePointer = - UnsafeMutablePointer.allocate(capacity: 1) -#endif - - /// Create a new lock. - public init() { -#if os(Windows) - InitializeSRWLock(self.mutex) -#else - var attr = pthread_mutexattr_t() - pthread_mutexattr_init(&attr) - debugOnly { - pthread_mutexattr_settype(&attr, .init(PTHREAD_MUTEX_ERRORCHECK)) - } - - let err = pthread_mutex_init(self.mutex, &attr) - precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") -#endif - } - - deinit { -#if os(Windows) - // SRWLOCK does not need to be free'd -#else - let err = pthread_mutex_destroy(self.mutex) - precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") -#endif - mutex.deallocate() - } - - /// Acquire the lock. - /// - /// Whenever possible, consider using `withLock` instead of this method and - /// `unlock`, to simplify lock handling. - public func lock() { -#if os(Windows) - AcquireSRWLockExclusive(self.mutex) -#else - let err = pthread_mutex_lock(self.mutex) - precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") -#endif - } - - /// Release the lock. - /// - /// Whenever possible, consider using `withLock` instead of this method and - /// `lock`, to simplify lock handling. - public func unlock() { -#if os(Windows) - ReleaseSRWLockExclusive(self.mutex) -#else - let err = pthread_mutex_unlock(self.mutex) - precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") -#endif - } - - /// Acquire the lock for the duration of the given block. - /// - /// This convenience method should be preferred to `lock` and `unlock` in - /// most situations, as it ensures that the lock will be released regardless - /// of how `body` exits. - /// - /// - Parameter body: The block to execute while holding the lock. - /// - Returns: The value returned by the block. - @inlinable - public func withLock(_ body: () throws -> T) rethrows -> T { - self.lock() - defer { - self.unlock() - } - return try body() - } - - // specialise Void return (for performance) - @inlinable - public func withLockVoid(_ body: () throws -> Void) rethrows -> Void { - try self.withLock(body) - } -} - -/// A `Lock` with a built-in state variable. -/// -/// This class provides a convenience addition to `Lock`: it provides the ability to wait -/// until the state variable is set to a specific value to acquire the lock. -public final class ConditionLock { - private var _value: T - private let mutex: NIOLock -#if os(Windows) - private let cond: UnsafeMutablePointer = - UnsafeMutablePointer.allocate(capacity: 1) -#else - private let cond: UnsafeMutablePointer = - UnsafeMutablePointer.allocate(capacity: 1) -#endif - - /// Create the lock, and initialize the state variable to `value`. - /// - /// - Parameter value: The initial value to give the state variable. - public init(value: T) { - self._value = value - self.mutex = NIOLock() -#if os(Windows) - InitializeConditionVariable(self.cond) -#else - let err = pthread_cond_init(self.cond, nil) - precondition(err == 0, "\(#function) failed in pthread_cond with error \(err)") -#endif - } - - deinit { -#if os(Windows) - // condition variables do not need to be explicitly destroyed -#else - let err = pthread_cond_destroy(self.cond) - precondition(err == 0, "\(#function) failed in pthread_cond with error \(err)") -#endif - self.cond.deallocate() - } - - /// Acquire the lock, regardless of the value of the state variable. - public func lock() { - self.mutex.lock() - } - - /// Release the lock, regardless of the value of the state variable. - public func unlock() { - self.mutex.unlock() - } - - /// The value of the state variable. - /// - /// Obtaining the value of the state variable requires acquiring the lock. - /// This means that it is not safe to access this property while holding the - /// lock: it is only safe to use it when not holding it. - public var value: T { - self.lock() - defer { - self.unlock() - } - return self._value - } - - /// Acquire the lock when the state variable is equal to `wantedValue`. - /// - /// - Parameter wantedValue: The value to wait for the state variable - /// to have before acquiring the lock. - public func lock(whenValue wantedValue: T) { - self.lock() - while true { - if self._value == wantedValue { - break - } - self.mutex.withLockPrimitive { mutex in -#if os(Windows) - let result = SleepConditionVariableSRW(self.cond, mutex, INFINITE, 0) - precondition(result, "\(#function) failed in SleepConditionVariableSRW with error \(GetLastError())") -#else - let err = pthread_cond_wait(self.cond, mutex) - precondition(err == 0, "\(#function) failed in pthread_cond with error \(err)") -#endif - } - } - } - - /// Acquire the lock when the state variable is equal to `wantedValue`, - /// waiting no more than `timeoutSeconds` seconds. - /// - /// - Parameter wantedValue: The value to wait for the state variable - /// to have before acquiring the lock. - /// - Parameter timeoutSeconds: The number of seconds to wait to acquire - /// the lock before giving up. - /// - Returns: `true` if the lock was acquired, `false` if the wait timed out. - public func lock(whenValue wantedValue: T, timeoutSeconds: Double) -> Bool { - precondition(timeoutSeconds >= 0) - -#if os(Windows) - var dwMilliseconds: DWORD = DWORD(timeoutSeconds * 1000) - - self.lock() - while true { - if self._value == wantedValue { - return true - } - - let dwWaitStart = timeGetTime() - if !SleepConditionVariableSRW(self.cond, self.mutex._storage.mutex, - dwMilliseconds, 0) { - let dwError = GetLastError() - if (dwError == ERROR_TIMEOUT) { - self.unlock() - return false - } - fatalError("SleepConditionVariableSRW: \(dwError)") - } - - // NOTE: this may be a spurious wakeup, adjust the timeout accordingly - dwMilliseconds = dwMilliseconds - (timeGetTime() - dwWaitStart) - } -#else - let nsecPerSec: Int64 = 1000000000 - self.lock() - /* the timeout as a (seconds, nano seconds) pair */ - let timeoutNS = Int64(timeoutSeconds * Double(nsecPerSec)) - - var curTime = timeval() - gettimeofday(&curTime, nil) - - let allNSecs: Int64 = timeoutNS + Int64(curTime.tv_usec) * 1000 - var timeoutAbs = timespec(tv_sec: curTime.tv_sec + Int((allNSecs / nsecPerSec)), - tv_nsec: Int(allNSecs % nsecPerSec)) - assert(timeoutAbs.tv_nsec >= 0 && timeoutAbs.tv_nsec < Int(nsecPerSec)) - assert(timeoutAbs.tv_sec >= curTime.tv_sec) - return self.mutex.withLockPrimitive { mutex -> Bool in - while true { - if self._value == wantedValue { - return true - } - switch pthread_cond_timedwait(self.cond, mutex, &timeoutAbs) { - case 0: - continue - case ETIMEDOUT: - self.unlock() - return false - case let e: - fatalError("caught error \(e) when calling pthread_cond_timedwait") - } - } - } -#endif - } - - /// Release the lock, setting the state variable to `newValue`. - /// - /// - Parameter newValue: The value to give to the state variable when we - /// release the lock. - public func unlock(withValue newValue: T) { - self._value = newValue - self.unlock() -#if os(Windows) - WakeAllConditionVariable(self.cond) -#else - let err = pthread_cond_broadcast(self.cond) - precondition(err == 0, "\(#function) failed in pthread_cond with error \(err)") -#endif - } -} - -/// A utility function that runs the body code only in debug builds, without -/// emitting compiler warnings. -/// -/// This is currently the only way to do this in Swift: see -/// https://forums.swift.org/t/support-debug-only-code/11037 for a discussion. -@inlinable -internal func debugOnly(_ body: () -> Void) { - assert({ body(); return true }()) -} - -@available(*, deprecated) -extension Lock: @unchecked Sendable {} -extension ConditionLock: @unchecked Sendable {} - -#if os(Windows) -@usableFromInline -typealias LockPrimitive = SRWLOCK -#else -@usableFromInline -typealias LockPrimitive = pthread_mutex_t -#endif - -@usableFromInline -enum LockOperations { } - -extension LockOperations { - @inlinable - static func create(_ mutex: UnsafeMutablePointer) { - mutex.assertValidAlignment() - -#if os(Windows) - InitializeSRWLock(mutex) -#else - var attr = pthread_mutexattr_t() - pthread_mutexattr_init(&attr) - debugOnly { - pthread_mutexattr_settype(&attr, .init(PTHREAD_MUTEX_ERRORCHECK)) - } - - let err = pthread_mutex_init(mutex, &attr) - precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") -#endif - } - - @inlinable - static func destroy(_ mutex: UnsafeMutablePointer) { - mutex.assertValidAlignment() - -#if os(Windows) - // SRWLOCK does not need to be free'd -#else - let err = pthread_mutex_destroy(mutex) - precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") -#endif - } - - @inlinable - static func lock(_ mutex: UnsafeMutablePointer) { - mutex.assertValidAlignment() - -#if os(Windows) - AcquireSRWLockExclusive(mutex) -#else - let err = pthread_mutex_lock(mutex) - precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") -#endif - } - - @inlinable - static func unlock(_ mutex: UnsafeMutablePointer) { - mutex.assertValidAlignment() - -#if os(Windows) - ReleaseSRWLockExclusive(mutex) -#else - let err = pthread_mutex_unlock(mutex) - precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") -#endif - } -} - -// Tail allocate both the mutex and a generic value using ManagedBuffer. -// Both the header pointer and the elements pointer are stable for -// the class's entire lifetime. -// -// However, for safety reasons, we elect to place the lock in the "elements" -// section of the buffer instead of the head. The reasoning here is subtle, -// so buckle in. -// -// _As a practical matter_, the implementation of ManagedBuffer ensures that -// the pointer to the header is stable across the lifetime of the class, and so -// each time you call `withUnsafeMutablePointers` or `withUnsafeMutablePointerToHeader` -// the value of the header pointer will be the same. This is because ManagedBuffer uses -// `Builtin.addressOf` to load the value of the header, and that does ~magic~ to ensure -// that it does not invoke any weird Swift accessors that might copy the value. -// -// _However_, the header is also available via the `.header` field on the ManagedBuffer. -// This presents a problem! The reason there's an issue is that `Builtin.addressOf` and friends -// do not interact with Swift's exclusivity model. That is, the various `with` functions do not -// conceptually trigger a mutating access to `.header`. For elements this isn't a concern because -// there's literally no other way to perform the access, but for `.header` it's entirely possible -// to accidentally recursively read it. -// -// Our implementation is free from these issues, so we don't _really_ need to worry about it. -// However, out of an abundance of caution, we store the Value in the header, and the LockPrimitive -// in the trailing elements. We still don't use `.header`, but it's better to be safe than sorry, -// and future maintainers will be happier that we were cautious. -// -// See also: https://github.com/apple/swift/pull/40000 -@usableFromInline -final class LockStorage: ManagedBuffer { - - @inlinable - static func create(value: Value) -> Self { - let buffer = Self.create(minimumCapacity: 1) { _ in - return value - } - let storage = unsafeDowncast(buffer, to: Self.self) - - storage.withUnsafeMutablePointers { _, lockPtr in - LockOperations.create(lockPtr) - } - - return storage - } - - @inlinable - func lock() { - self.withUnsafeMutablePointerToElements { lockPtr in - LockOperations.lock(lockPtr) - } - } - - @inlinable - func unlock() { - self.withUnsafeMutablePointerToElements { lockPtr in - LockOperations.unlock(lockPtr) - } - } - - @inlinable - deinit { - self.withUnsafeMutablePointerToElements { lockPtr in - LockOperations.destroy(lockPtr) - } - } - - @inlinable - func withLockPrimitive(_ body: (UnsafeMutablePointer) throws -> T) rethrows -> T { - try self.withUnsafeMutablePointerToElements { lockPtr in - return try body(lockPtr) - } - } - - @inlinable - func withLockedValue(_ mutate: (inout Value) throws -> T) rethrows -> T { - try self.withUnsafeMutablePointers { valuePtr, lockPtr in - LockOperations.lock(lockPtr) - defer { LockOperations.unlock(lockPtr) } - return try mutate(&valuePtr.pointee) - } - } -} - -extension LockStorage: @unchecked Sendable { } - -/// A threading lock based on `libpthread` instead of `libdispatch`. -/// -/// - note: ``NIOLock`` has reference semantics. -/// -/// This object provides a lock on top of a single `pthread_mutex_t`. This kind -/// of lock is safe to use with `libpthread`-based threading models, such as the -/// one used by NIO. On Windows, the lock is based on the substantially similar -/// `SRWLOCK` type. -public struct NIOLock { - @usableFromInline - internal let _storage: LockStorage - - /// Create a new lock. - @inlinable - public init() { - self._storage = .create(value: ()) - } - - /// Acquire the lock. - /// - /// Whenever possible, consider using `withLock` instead of this method and - /// `unlock`, to simplify lock handling. - @inlinable - public func lock() { - self._storage.lock() - } - - /// Release the lock. - /// - /// Whenever possible, consider using `withLock` instead of this method and - /// `lock`, to simplify lock handling. - @inlinable - public func unlock() { - self._storage.unlock() - } - - @inlinable - internal func withLockPrimitive(_ body: (UnsafeMutablePointer) throws -> T) rethrows -> T { - return try self._storage.withLockPrimitive(body) - } -} - -extension NIOLock { - /// Acquire the lock for the duration of the given block. - /// - /// This convenience method should be preferred to `lock` and `unlock` in - /// most situations, as it ensures that the lock will be released regardless - /// of how `body` exits. - /// - /// - Parameter body: The block to execute while holding the lock. - /// - Returns: The value returned by the block. - @inlinable - public func withLock(_ body: () throws -> T) rethrows -> T { - self.lock() - defer { - self.unlock() - } - return try body() - } - - @inlinable - public func withLockVoid(_ body: () throws -> Void) rethrows -> Void { - try self.withLock(body) - } -} - -extension NIOLock: Sendable {} - -extension UnsafeMutablePointer { - @inlinable - func assertValidAlignment() { - assert(UInt(bitPattern: self) % UInt(MemoryLayout.alignment) == 0) - } -} diff --git a/Sources/_CShims/include/process_shims.h b/Sources/_SubprocessCShims/include/process_shims.h similarity index 74% rename from Sources/_CShims/include/process_shims.h rename to Sources/_SubprocessCShims/include/process_shims.h index 563b517f8..35cbd2fe3 100644 --- a/Sources/_CShims/include/process_shims.h +++ b/Sources/_SubprocessCShims/include/process_shims.h @@ -2,20 +2,17 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2024 Apple Inc. and the Swift.org project authors -// Licensed under Apache License v2.0 +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception // -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift.org project authors -// -// SPDX-License-Identifier: Apache-2.0 +// See https://swift.org/LICENSE.txt for license information // //===----------------------------------------------------------------------===// #ifndef process_shims_h #define process_shims_h -#include "_CShimsTargetConditionals.h" +#include "target_conditionals.h" #if !TARGET_OS_WINDOWS #include @@ -24,6 +21,10 @@ #include #endif +#if __has_include() +vm_size_t _subprocess_vm_size(void); +#endif + #if TARGET_OS_MAC int _subprocess_spawn( pid_t * _Nonnull pid, @@ -60,6 +61,10 @@ int _was_process_signaled(int status); int _get_signal_code(int status); int _was_process_suspended(int status); +void _subprocess_lock_environ(void); +void _subprocess_unlock_environ(void); +char * _Nullable * _Nullable _subprocess_get_environ(void); + #if TARGET_OS_LINUX int _shims_snprintf( char * _Nonnull str, @@ -72,4 +77,15 @@ int _shims_snprintf( #endif // !TARGET_OS_WINDOWS +#if TARGET_OS_WINDOWS + +#ifndef _WINDEF_ +typedef unsigned long DWORD; +typedef int BOOL; +#endif + +BOOL _subprocess_windows_send_vm_close(DWORD pid); + +#endif + #endif /* process_shims_h */ diff --git a/Sources/_CShims/include/_CShimsTargetConditionals.h b/Sources/_SubprocessCShims/include/target_conditionals.h similarity index 76% rename from Sources/_CShims/include/_CShimsTargetConditionals.h rename to Sources/_SubprocessCShims/include/target_conditionals.h index 9e1d80cb7..fef2eaf2e 100644 --- a/Sources/_CShims/include/_CShimsTargetConditionals.h +++ b/Sources/_SubprocessCShims/include/target_conditionals.h @@ -2,13 +2,11 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2024 Apple Inc. and the Swift.org project authors -// Licensed under Apache License v2.0 +// Copyright (c) 2021 - 2022 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception // -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift.org project authors -// -// SPDX-License-Identifier: Apache-2.0 +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// diff --git a/Sources/_SubprocessCShims/process_shims.c b/Sources/_SubprocessCShims/process_shims.c new file mode 100644 index 000000000..287e1a8d0 --- /dev/null +++ b/Sources/_SubprocessCShims/process_shims.c @@ -0,0 +1,682 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +#include "include/target_conditionals.h" + +#if TARGET_OS_LINUX +// For posix_spawn_file_actions_addchdir_np +#define _GNU_SOURCE 1 +#endif + +#include "include/process_shims.h" + +#if TARGET_OS_WINDOWS +#include +#else +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#if __has_include() +#include +#elif defined(_WIN32) +#include +#elif __has_include() +#include +extern char **environ; +#endif + +int _was_process_exited(int status) { + return WIFEXITED(status); +} + +int _get_exit_code(int status) { + return WEXITSTATUS(status); +} + +int _was_process_signaled(int status) { + return WIFSIGNALED(status); +} + +int _get_signal_code(int status) { + return WTERMSIG(status); +} + +int _was_process_suspended(int status) { + return WIFSTOPPED(status); +} + +#if TARGET_OS_LINUX +#include + +int _shims_snprintf( + char * _Nonnull str, + int len, + const char * _Nonnull format, + char * _Nonnull str1, + char * _Nonnull str2 +) { + return snprintf(str, len, format, str1, str2); +} +#endif + +#if __has_include() +vm_size_t _subprocess_vm_size(void) { + // This shim exists because vm_page_size is not marked const, and therefore looks like global mutable state to Swift. + return vm_page_size; +} +#endif + +// MARK: - Darwin (posix_spawn) +#if TARGET_OS_MAC +static int _subprocess_spawn_prefork( + pid_t * _Nonnull pid, + const char * _Nonnull exec_path, + const posix_spawn_file_actions_t _Nullable * _Nonnull file_actions, + const posix_spawnattr_t _Nullable * _Nonnull spawn_attrs, + char * _Nullable const args[_Nonnull], + char * _Nullable const env[_Nullable], + uid_t * _Nullable uid, + gid_t * _Nullable gid, + int number_of_sgroups, const gid_t * _Nullable sgroups, + int create_session +) { + // Set `POSIX_SPAWN_SETEXEC` flag since we are forking ourselves + short flags = 0; + int rc = posix_spawnattr_getflags(spawn_attrs, &flags); + if (rc != 0) { + return rc; + } + + rc = posix_spawnattr_setflags( + (posix_spawnattr_t *)spawn_attrs, flags | POSIX_SPAWN_SETEXEC + ); + if (rc != 0) { + return rc; + } + // Setup pipe to catch exec failures from child + int pipefd[2]; + if (pipe(pipefd) != 0) { + return errno; + } + // Set FD_CLOEXEC so the pipe is automatically closed when exec succeeds + flags = fcntl(pipefd[0], F_GETFD); + if (flags == -1) { + close(pipefd[0]); + close(pipefd[1]); + return errno; + } + flags |= FD_CLOEXEC; + if (fcntl(pipefd[0], F_SETFD, flags) == -1) { + close(pipefd[0]); + close(pipefd[1]); + return errno; + } + + flags = fcntl(pipefd[1], F_GETFD); + if (flags == -1) { + close(pipefd[0]); + close(pipefd[1]); + return errno; + } + flags |= FD_CLOEXEC; + if (fcntl(pipefd[1], F_SETFD, flags) == -1) { + close(pipefd[0]); + close(pipefd[1]); + return errno; + } + + // Finally, fork +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated" + pid_t childPid = fork(); +#pragma GCC diagnostic pop + if (childPid == -1) { + close(pipefd[0]); + close(pipefd[1]); + return errno; + } + + if (childPid == 0) { + // Child process + close(pipefd[0]); // Close unused read end + + // Perform setups + if (number_of_sgroups > 0 && sgroups != NULL) { + if (setgroups(number_of_sgroups, sgroups) != 0) { + int error = errno; + write(pipefd[1], &error, sizeof(error)); + close(pipefd[1]); + _exit(EXIT_FAILURE); + } + } + + if (uid != NULL) { + if (setuid(*uid) != 0) { + int error = errno; + write(pipefd[1], &error, sizeof(error)); + close(pipefd[1]); + _exit(EXIT_FAILURE); + } + } + + if (gid != NULL) { + if (setgid(*gid) != 0) { + int error = errno; + write(pipefd[1], &error, sizeof(error)); + close(pipefd[1]); + _exit(EXIT_FAILURE); + } + } + + if (create_session != 0) { + (void)setsid(); + } + + // Use posix_spawnas exec + int error = posix_spawn(pid, exec_path, file_actions, spawn_attrs, args, env); + // If we reached this point, something went wrong + write(pipefd[1], &error, sizeof(error)); + close(pipefd[1]); + _exit(EXIT_FAILURE); + } else { + // Parent process + close(pipefd[1]); // Close unused write end + // Communicate child pid back + *pid = childPid; + // Read from the pipe until pipe is closed + // Eitehr due to exec succeeds or error is written + int childError = 0; + if (read(pipefd[0], &childError, sizeof(childError)) > 0) { + // We encountered error + close(pipefd[0]); + return childError; + } else { + // Child process exec was successful + close(pipefd[0]); + return 0; + } + } +} + +int _subprocess_spawn( + pid_t * _Nonnull pid, + const char * _Nonnull exec_path, + const posix_spawn_file_actions_t _Nullable * _Nonnull file_actions, + const posix_spawnattr_t _Nullable * _Nonnull spawn_attrs, + char * _Nullable const args[_Nonnull], + char * _Nullable const env[_Nullable], + uid_t * _Nullable uid, + gid_t * _Nullable gid, + int number_of_sgroups, const gid_t * _Nullable sgroups, + int create_session +) { + int require_pre_fork = uid != NULL || + gid != NULL || + number_of_sgroups > 0 || + create_session > 0; + + if (require_pre_fork != 0) { + int rc = _subprocess_spawn_prefork( + pid, + exec_path, + file_actions, spawn_attrs, + args, env, + uid, gid, number_of_sgroups, sgroups, create_session + ); + return rc; + } + + // Spawn + return posix_spawn(pid, exec_path, file_actions, spawn_attrs, args, env); +} + +#endif // TARGET_OS_MAC + +// MARK: - Linux (fork/exec + posix_spawn fallback) +#if TARGET_OS_LINUX + +#if _POSIX_SPAWN +static int _subprocess_is_addchdir_np_available() { +#if defined(__GLIBC__) && !__GLIBC_PREREQ(2, 29) + // Glibc versions prior to 2.29 don't support posix_spawn_file_actions_addchdir_np, impacting: + // - Amazon Linux 2 (EoL mid-2025) + return 0; +#elif defined(__OpenBSD__) || defined(__QNX__) + // Currently missing as of: + // - OpenBSD 7.5 (April 2024) + // - QNX 8 (December 2023) + return 0; +#elif defined(__GLIBC__) || TARGET_OS_DARWIN || defined(__FreeBSD__) || (defined(__ANDROID__) && __ANDROID_API__ >= 34) || defined(__musl__) + // Pre-standard posix_spawn_file_actions_addchdir_np version available in: + // - Solaris 11.3 (October 2015) + // - Glibc 2.29 (February 2019) + // - macOS 10.15 (October 2019) + // - musl 1.1.24 (October 2019) + // - FreeBSD 13.1 (May 2022) + // - Android 14 (October 2023) + return 1; +#else + // Standardized posix_spawn_file_actions_addchdir version (POSIX.1-2024, June 2024) available in: + // - Solaris 11.4 (August 2018) + // - NetBSD 10.0 (March 2024) + return 1; +#endif +} + +static int _subprocess_addchdir_np( + posix_spawn_file_actions_t *file_actions, + const char * __restrict path +) { +#if defined(__GLIBC__) && !__GLIBC_PREREQ(2, 29) + // Glibc versions prior to 2.29 don't support posix_spawn_file_actions_addchdir_np, impacting: + // - Amazon Linux 2 (EoL mid-2025) + // noop +#elif defined(__OpenBSD__) || defined(__QNX__) + // Currently missing as of: + // - OpenBSD 7.5 (April 2024) + // - QNX 8 (December 2023) + // noop +#elif defined(__GLIBC__) || TARGET_OS_DARWIN || defined(__FreeBSD__) || (defined(__ANDROID__) && __ANDROID_API__ >= 34) || defined(__musl__) + // Pre-standard posix_spawn_file_actions_addchdir_np version available in: + // - Solaris 11.3 (October 2015) + // - Glibc 2.29 (February 2019) + // - macOS 10.15 (October 2019) + // - musl 1.1.24 (October 2019) + // - FreeBSD 13.1 (May 2022) + // - Android 14 (October 2023) + return posix_spawn_file_actions_addchdir_np(file_actions, path); +#else + // Standardized posix_spawn_file_actions_addchdir version (POSIX.1-2024, June 2024) available in: + // - Solaris 11.4 (August 2018) + // - NetBSD 10.0 (March 2024) + return posix_spawn_file_actions_addchdir(file_actions, path); +#endif +} + +static int _subprocess_posix_spawn_fallback( + pid_t * _Nonnull pid, + const char * _Nonnull exec_path, + const char * _Nullable working_directory, + const int file_descriptors[_Nonnull], + char * _Nullable const args[_Nonnull], + char * _Nullable const env[_Nullable], + gid_t * _Nullable process_group_id +) { + // Setup stdin, stdout, and stderr + posix_spawn_file_actions_t file_actions; + + int rc = posix_spawn_file_actions_init(&file_actions); + if (rc != 0) { return rc; } + if (file_descriptors[0] >= 0) { + rc = posix_spawn_file_actions_adddup2( + &file_actions, file_descriptors[0], STDIN_FILENO + ); + if (rc != 0) { return rc; } + } + if (file_descriptors[2] >= 0) { + rc = posix_spawn_file_actions_adddup2( + &file_actions, file_descriptors[2], STDOUT_FILENO + ); + if (rc != 0) { return rc; } + } + if (file_descriptors[4] >= 0) { + rc = posix_spawn_file_actions_adddup2( + &file_actions, file_descriptors[4], STDERR_FILENO + ); + if (rc != 0) { return rc; } + } + // Setup working directory + rc = _subprocess_addchdir_np(&file_actions, working_directory); + if (rc != 0) { + return rc; + } + + // Close parent side + if (file_descriptors[1] >= 0) { + rc = posix_spawn_file_actions_addclose(&file_actions, file_descriptors[1]); + if (rc != 0) { return rc; } + } + if (file_descriptors[3] >= 0) { + rc = posix_spawn_file_actions_addclose(&file_actions, file_descriptors[3]); + if (rc != 0) { return rc; } + } + if (file_descriptors[5] >= 0) { + rc = posix_spawn_file_actions_addclose(&file_actions, file_descriptors[5]); + if (rc != 0) { return rc; } + } + + // Setup spawnattr + posix_spawnattr_t spawn_attr; + rc = posix_spawnattr_init(&spawn_attr); + if (rc != 0) { return rc; } + // Masks + sigset_t no_signals; + sigset_t all_signals; + sigemptyset(&no_signals); + sigfillset(&all_signals); + rc = posix_spawnattr_setsigmask(&spawn_attr, &no_signals); + if (rc != 0) { return rc; } + rc = posix_spawnattr_setsigdefault(&spawn_attr, &all_signals); + if (rc != 0) { return rc; } + // Flags + short flags = POSIX_SPAWN_SETSIGMASK | POSIX_SPAWN_SETSIGDEF; + if (process_group_id != NULL) { + flags |= POSIX_SPAWN_SETPGROUP; + rc = posix_spawnattr_setpgroup(&spawn_attr, *process_group_id); + if (rc != 0) { return rc; } + } + rc = posix_spawnattr_setflags(&spawn_attr, flags); + + // Spawn! + rc = posix_spawn( + pid, exec_path, + &file_actions, &spawn_attr, + args, env + ); + posix_spawn_file_actions_destroy(&file_actions); + posix_spawnattr_destroy(&spawn_attr); + return rc; +} +#endif // _POSIX_SPAWN + +int _subprocess_fork_exec( + pid_t * _Nonnull pid, + const char * _Nonnull exec_path, + const char * _Nullable working_directory, + const int file_descriptors[_Nonnull], + char * _Nullable const args[_Nonnull], + char * _Nullable const env[_Nullable], + uid_t * _Nullable uid, + gid_t * _Nullable gid, + gid_t * _Nullable process_group_id, + int number_of_sgroups, const gid_t * _Nullable sgroups, + int create_session, + void (* _Nullable configurator)(void) +) { + int require_pre_fork = _subprocess_is_addchdir_np_available() == 0 || + uid != NULL || + gid != NULL || + process_group_id != NULL || + (number_of_sgroups > 0 && sgroups != NULL) || + create_session || + configurator != NULL; + +#if _POSIX_SPAWN + // If posix_spawn is available on this platform and + // we do not require prefork, use posix_spawn if possible. + // + // (Glibc's posix_spawn does not support + // `POSIX_SPAWN_SETEXEC` therefore we have to keep + // using fork/exec if `require_pre_fork` is true. + if (require_pre_fork == 0) { + return _subprocess_posix_spawn_fallback( + pid, exec_path, + working_directory, + file_descriptors, + args, env, + process_group_id + ); + } +#endif + + // Setup pipe to catch exec failures from child + int pipefd[2]; + if (pipe(pipefd) != 0) { + return errno; + } + // Set FD_CLOEXEC so the pipe is automatically closed when exec succeeds + short flags = fcntl(pipefd[0], F_GETFD); + if (flags == -1) { + close(pipefd[0]); + close(pipefd[1]); + return errno; + } + flags |= FD_CLOEXEC; + if (fcntl(pipefd[0], F_SETFD, flags) == -1) { + close(pipefd[0]); + close(pipefd[1]); + return errno; + } + + flags = fcntl(pipefd[1], F_GETFD); + if (flags == -1) { + close(pipefd[0]); + close(pipefd[1]); + return errno; + } + flags |= FD_CLOEXEC; + if (fcntl(pipefd[1], F_SETFD, flags) == -1) { + close(pipefd[0]); + close(pipefd[1]); + return errno; + } + + // Finally, fork +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated" + pid_t childPid = fork(); +#pragma GCC diagnostic pop + if (childPid == -1) { + close(pipefd[0]); + close(pipefd[1]); + return errno; + } + + if (childPid == 0) { + // Child process + close(pipefd[0]); // Close unused read end + + // Perform setups + if (working_directory != NULL) { + if (chdir(working_directory) != 0) { + int error = errno; + write(pipefd[1], &error, sizeof(error)); + close(pipefd[1]); + _exit(EXIT_FAILURE); + } + } + + + if (uid != NULL) { + if (setuid(*uid) != 0) { + int error = errno; + write(pipefd[1], &error, sizeof(error)); + close(pipefd[1]); + _exit(EXIT_FAILURE); + } + } + + if (gid != NULL) { + if (setgid(*gid) != 0) { + int error = errno; + write(pipefd[1], &error, sizeof(error)); + close(pipefd[1]); + _exit(EXIT_FAILURE); + } + } + + if (number_of_sgroups > 0 && sgroups != NULL) { + if (setgroups(number_of_sgroups, sgroups) != 0) { + int error = errno; + write(pipefd[1], &error, sizeof(error)); + close(pipefd[1]); + _exit(EXIT_FAILURE); + } + } + + if (create_session != 0) { + (void)setsid(); + } + + if (process_group_id != NULL) { + (void)setpgid(0, *process_group_id); + } + + // Bind stdin, stdout, and stderr + int rc = 0; + if (file_descriptors[0] >= 0) { + rc = dup2(file_descriptors[0], STDIN_FILENO); + if (rc < 0) { + int error = errno; + write(pipefd[1], &error, sizeof(error)); + close(pipefd[1]); + _exit(EXIT_FAILURE); + } + } + if (file_descriptors[2] >= 0) { + rc = dup2(file_descriptors[2], STDOUT_FILENO); + if (rc < 0) { + int error = errno; + write(pipefd[1], &error, sizeof(error)); + close(pipefd[1]); + _exit(EXIT_FAILURE); + } + } + if (file_descriptors[4] >= 0) { + rc = dup2(file_descriptors[4], STDERR_FILENO); + if (rc < 0) { + int error = errno; + write(pipefd[1], &error, sizeof(error)); + close(pipefd[1]); + _exit(EXIT_FAILURE); + } + } + // Close parent side + if (file_descriptors[1] >= 0) { + rc = close(file_descriptors[1]); + } + if (file_descriptors[3] >= 0) { + rc = close(file_descriptors[3]); + } + if (file_descriptors[4] >= 0) { + rc = close(file_descriptors[5]); + } + if (rc != 0) { + int error = errno; + write(pipefd[1], &error, sizeof(error)); + close(pipefd[1]); + _exit(EXIT_FAILURE); + } + // Run custom configuratior + if (configurator != NULL) { + configurator(); + } + // Finally, exec + execve(exec_path, args, env); + // If we reached this point, something went wrong + int error = errno; + write(pipefd[1], &error, sizeof(error)); + close(pipefd[1]); + _exit(EXIT_FAILURE); + } else { + // Parent process + close(pipefd[1]); // Close unused write end + // Communicate child pid back + *pid = childPid; + // Read from the pipe until pipe is closed + // Eitehr due to exec succeeds or error is written + int childError = 0; + if (read(pipefd[0], &childError, sizeof(childError)) > 0) { + // We encountered error + close(pipefd[0]); + return childError; + } else { + // Child process exec was successful + close(pipefd[0]); + return 0; + } + } +} + +#endif // TARGET_OS_LINUX + +#endif // !TARGET_OS_WINDOWS + +#pragma mark - Environment Locking + +#if __has_include() +#import +void _subprocess_lock_environ(void) { + environ_lock_np(); +} + +void _subprocess_unlock_environ(void) { + environ_unlock_np(); +} +#else +void _subprocess_lock_environ(void) { /* noop */ } +void _subprocess_unlock_environ(void) { /* noop */ } +#endif + +char ** _subprocess_get_environ(void) { +#if __has_include() + return *_NSGetEnviron(); +#elif defined(_WIN32) +#include + return _environ; +#elif TARGET_OS_WASI + return __wasilibc_get_environ(); +#elif __has_include() + return environ; +#endif +} + + +#if TARGET_OS_WINDOWS + +typedef struct { + DWORD pid; + HWND mainWindow; +} CallbackContext; + +static BOOL CALLBACK enumWindowsCallback( + HWND hwnd, + LPARAM lParam +) { + CallbackContext *context = (CallbackContext *)lParam; + DWORD pid; + GetWindowThreadProcessId(hwnd, &pid); + if (pid == context->pid) { + context->mainWindow = hwnd; + return FALSE; // Stop enumeration + } + return TRUE; // Continue enumeration +} + +BOOL _subprocess_windows_send_vm_close( + DWORD pid +) { + // First attempt to find the Window associate + // with this process + CallbackContext context = {0}; + context.pid = pid; + EnumWindows(enumWindowsCallback, (LPARAM)&context); + + if (context.mainWindow != NULL) { + if (SendMessage(context.mainWindow, WM_CLOSE, 0, 0)) { + return TRUE; + } + } + + return FALSE; +} + +#endif + diff --git a/SwiftKit/src/main/java/org/swift/swiftkit/SwiftInstanceCleanup.java b/SwiftKit/src/main/java/org/swift/swiftkit/SwiftInstanceCleanup.java deleted file mode 100644 index a9fdd9c8f..000000000 --- a/SwiftKit/src/main/java/org/swift/swiftkit/SwiftInstanceCleanup.java +++ /dev/null @@ -1,38 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift.org project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift.org project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -package org.swift.swiftkit; - -import java.lang.foreign.MemorySegment; - -/** - * A Swift memory instance cleanup, e.g. count-down a reference count and destroy a class, or destroy struct/enum etc. - */ -record SwiftInstanceCleanup( - MemorySegment selfPointer, - SwiftAnyType selfType, - Runnable markAsDestroyed -) implements Runnable { - - @Override - public void run() { - markAsDestroyed.run(); - - // Allow null pointers just for AutoArena tests. - if (selfType != null && selfPointer != null) { - System.out.println("[debug] Destroy swift value [" + selfType.getSwiftName() + "]: " + selfPointer); - SwiftValueWitnessTable.destroy(selfType, selfPointer); - } - } -} diff --git a/SwiftKit/src/test/java/org/swift/swiftkit/SwiftRuntimeMetadataTest.java b/SwiftKit/src/test/java/org/swift/swiftkit/SwiftRuntimeMetadataTest.java deleted file mode 100644 index ffefe72ed..000000000 --- a/SwiftKit/src/test/java/org/swift/swiftkit/SwiftRuntimeMetadataTest.java +++ /dev/null @@ -1,61 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift.org project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift.org project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -package org.swift.swiftkit; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -public class SwiftRuntimeMetadataTest { - - @Test - public void integer_layout_metadata() { - SwiftAnyType swiftType = SwiftKit.getTypeByMangledNameInEnvironment("Si").get(); - - if (SwiftValueLayout.addressByteSize() == 4) { - // 32-bit platform - Assertions.assertEquals(8, SwiftValueWitnessTable.sizeOfSwiftType(swiftType.$memorySegment())); - Assertions.assertEquals(8, SwiftValueWitnessTable.strideOfSwiftType(swiftType.$memorySegment())); - Assertions.assertEquals(8, SwiftValueWitnessTable.alignmentOfSwiftType(swiftType.$memorySegment())); - Assertions.assertEquals("[8%[9:b1]x7](Swift.Int)", SwiftValueWitnessTable.layoutOfSwiftType(swiftType.$memorySegment()).toString()); - } else { - // 64-bit platform - Assertions.assertEquals(8, SwiftValueWitnessTable.sizeOfSwiftType(swiftType.$memorySegment())); - Assertions.assertEquals(8, SwiftValueWitnessTable.strideOfSwiftType(swiftType.$memorySegment())); - Assertions.assertEquals(8, SwiftValueWitnessTable.alignmentOfSwiftType(swiftType.$memorySegment())); - Assertions.assertEquals("[8%[8:b1]](Swift.Int)", SwiftValueWitnessTable.layoutOfSwiftType(swiftType.$memorySegment()).toString()); - } - } - - @Test - public void optional_integer_layout_metadata() { - SwiftAnyType swiftType = SwiftKit.getTypeByMangledNameInEnvironment("SiSg").get(); - - if (SwiftValueLayout.addressByteSize() == 4) { - // 64-bit platform - Assertions.assertEquals(9, SwiftValueWitnessTable.sizeOfSwiftType(swiftType.$memorySegment())); - Assertions.assertEquals(16, SwiftValueWitnessTable.strideOfSwiftType(swiftType.$memorySegment())); - Assertions.assertEquals(8, SwiftValueWitnessTable.alignmentOfSwiftType(swiftType.$memorySegment())); - Assertions.assertEquals("[8%[9:b1]x7](Swift.Optional)", SwiftValueWitnessTable.layoutOfSwiftType(swiftType.$memorySegment()).toString()); - } else { - // 64-bit platform - Assertions.assertEquals(9, SwiftValueWitnessTable.sizeOfSwiftType(swiftType.$memorySegment())); - Assertions.assertEquals(16, SwiftValueWitnessTable.strideOfSwiftType(swiftType.$memorySegment())); - Assertions.assertEquals(8, SwiftValueWitnessTable.alignmentOfSwiftType(swiftType.$memorySegment())); - Assertions.assertEquals("[8%[9:b1]x7](Swift.Optional)", SwiftValueWitnessTable.layoutOfSwiftType(swiftType.$memorySegment()).toString()); - } - } - -} diff --git a/SwiftKit/build.gradle b/SwiftKitCore/build.gradle similarity index 55% rename from SwiftKit/build.gradle rename to SwiftKitCore/build.gradle index e0896e668..bc6bd2794 100644 --- a/SwiftKit/build.gradle +++ b/SwiftKitCore/build.gradle @@ -14,26 +14,58 @@ plugins { id("build-logic.java-application-conventions") + id("me.champeau.jmh") version "0.7.2" + id("maven-publish") } group = "org.swift.swiftkit" version = "1.0-SNAPSHOT" +base { + archivesName = "swiftkit-core" +} repositories { + mavenLocal() mavenCentral() } +publishing { + publications { + maven(MavenPublication) { + groupId = group + artifactId = 'swiftkit-core' + version = '1.0-SNAPSHOT' + + from components.java + } + } +} + java { toolchain { - languageVersion.set(JavaLanguageVersion.of(22)) + languageVersion.set(JavaLanguageVersion.of(25)) } } +tasks.withType(JavaCompile).configureEach { + // Support Java 11 + options.release.set(11) +} + dependencies { + testRuntimeOnly("org.junit.platform:junit-platform-launcher") // TODO: workaround for not finding junit: https://github.com/gradle/gradle/issues/34512 testImplementation(platform("org.junit:junit-bom:5.10.0")) testImplementation("org.junit.jupiter:junit-jupiter") } +testing { + suites { + test { + useJUnitJupiter('5.10.3') + } + } +} + tasks.test { useJUnitPlatform() testLogging { @@ -41,10 +73,10 @@ tasks.test { } } -// SwiftKit depends on SwiftKitSwift (Swift library that this Java library calls into) +// SwiftKit depends on SwiftRuntimeFunctions (Swift library that this Java library calls into) def compileSwift = tasks.register("compileSwift", Exec) { - description "Compile the swift-java SwiftKitSwift dynamic library that SwiftKit (Java) calls into" + description "Compile the swift-java SwiftRuntimeFunctions dynamic library that SwiftKit (Java) calls into" inputs.file(new File(rootDir, "Package.swift")) inputs.dir(new File(rootDir, "Sources/")) // a bit generous, but better safe than sorry, and include all Swift source changes @@ -52,7 +84,8 @@ def compileSwift = tasks.register("compileSwift", Exec) { workingDir = rootDir commandLine "swift" - args("build", "--target", "SwiftKitSwift") + // FIXME: disable prebuilts until swift-syntax isn't broken on 6.2 anymore: https://github.com/swiftlang/swift-java/issues/418 + args("build", "--disable-experimental-prebuilts", "--target", "SwiftRuntimeFunctions") } tasks.build { dependsOn("compileSwift") diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/AutoSwiftMemorySession.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/AutoSwiftMemorySession.java new file mode 100644 index 000000000..36e732097 --- /dev/null +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/AutoSwiftMemorySession.java @@ -0,0 +1,56 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package org.swift.swiftkit.core; + + +import org.swift.swiftkit.core.ref.SwiftCleaner; + +import java.util.Objects; +import java.util.concurrent.ThreadFactory; + +/** + * A memory session which manages registered objects via the Garbage Collector. + * + *

When registered Java wrapper classes around native Swift instances {@link SwiftInstance}, + * are eligible for collection, this will trigger the cleanup of the native resources as well. + * + *

This memory session is LESS reliable than using a {@link ConfinedSwiftMemorySession} because + * the timing of when the native resources are cleaned up is somewhat undefined, and rely on the + * system GC. Meaning, that if an object nas been promoted to an old generation, there may be a + * long time between the resource no longer being referenced "in Java" and its native memory being released, + * and also the deinit of the Swift type being run. + * + *

This can be problematic for Swift applications which rely on quick release of resources, and may expect + * the deinits to run in expected and "quick" succession. + * + *

Whenever possible, prefer using an explicitly managed {@link SwiftArena}, such as {@link SwiftArena#ofConfined()}. + */ +final class AutoSwiftMemorySession implements SwiftArena { + private final SwiftCleaner swiftCleaner; + + public AutoSwiftMemorySession(ThreadFactory cleanerThreadFactory) { + this.swiftCleaner = SwiftCleaner.create(cleanerThreadFactory); + } + + @Override + public void register(SwiftInstance instance) { + Objects.requireNonNull(instance, "value"); + + // We make sure we don't capture `instance` in the + // cleanup action, so we can ignore the warning below. + var cleanupAction = instance.$createCleanup(); + swiftCleaner.register(instance, cleanupAction); + } +} \ No newline at end of file diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/CallTraces.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/CallTraces.java new file mode 100644 index 000000000..358205ffd --- /dev/null +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/CallTraces.java @@ -0,0 +1,61 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package org.swift.swiftkit.core; + +public class CallTraces { + public static final boolean TRACE_DOWNCALLS = + Boolean.getBoolean("jextract.trace.downcalls"); + + // Used to manually debug with complete backtraces on every traceDowncall + public static final boolean TRACE_DOWNCALLS_FULL = false; + + public static void traceDowncall(Object... args) { + RuntimeException ex = new RuntimeException(); + + String traceArgs = joinArgs(args); + System.err.printf("[java][%s:%d] Downcall: %s.%s(%s)\n", + ex.getStackTrace()[1].getFileName(), + ex.getStackTrace()[1].getLineNumber(), + ex.getStackTrace()[1].getClassName(), + ex.getStackTrace()[1].getMethodName(), + traceArgs); + if (TRACE_DOWNCALLS_FULL) { + ex.printStackTrace(); + } + } + + public static void trace(Object... args) { + RuntimeException ex = new RuntimeException(); + + String traceArgs = joinArgs(args); + System.err.printf("[java][%s:%d] %s: %s\n", + ex.getStackTrace()[1].getFileName(), + ex.getStackTrace()[1].getLineNumber(), + ex.getStackTrace()[1].getMethodName(), + traceArgs); + } + + private static String joinArgs(Object[] args) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < args.length; i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(args[i].toString()); + } + return sb.toString(); + } + +} diff --git a/SwiftKit/src/main/java/org/swift/swiftkit/ClosableSwiftArena.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/ClosableSwiftArena.java similarity index 96% rename from SwiftKit/src/main/java/org/swift/swiftkit/ClosableSwiftArena.java rename to SwiftKitCore/src/main/java/org/swift/swiftkit/core/ClosableSwiftArena.java index c257ae578..1a0890690 100644 --- a/SwiftKit/src/main/java/org/swift/swiftkit/ClosableSwiftArena.java +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/ClosableSwiftArena.java @@ -12,7 +12,7 @@ // //===----------------------------------------------------------------------===// -package org.swift.swiftkit; +package org.swift.swiftkit.core; /** * Auto-closable version of {@link SwiftArena}. @@ -24,5 +24,4 @@ public interface ClosableSwiftArena extends SwiftArena, AutoCloseable { * Throws if unable to verify all resources have been release (e.g. over retained Swift classes) */ void close(); - } diff --git a/SwiftKit/src/main/java/org/swift/swiftkit/ConfinedSwiftMemorySession.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/ConfinedSwiftMemorySession.java similarity index 54% rename from SwiftKit/src/main/java/org/swift/swiftkit/ConfinedSwiftMemorySession.java rename to SwiftKitCore/src/main/java/org/swift/swiftkit/core/ConfinedSwiftMemorySession.java index 86725ae8e..3514d9c16 100644 --- a/SwiftKit/src/main/java/org/swift/swiftkit/ConfinedSwiftMemorySession.java +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/ConfinedSwiftMemorySession.java @@ -12,38 +12,30 @@ // //===----------------------------------------------------------------------===// -package org.swift.swiftkit; +package org.swift.swiftkit.core; -import java.lang.foreign.Arena; -import java.lang.foreign.MemorySegment; import java.util.LinkedList; import java.util.List; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicInteger; -final class ConfinedSwiftMemorySession implements ClosableSwiftArena { +public class ConfinedSwiftMemorySession implements ClosableSwiftArena { final static int CLOSED = 0; final static int ACTIVE = 1; - final Thread owner; final AtomicInteger state; - final Arena arena; final ConfinedResourceList resources; - public ConfinedSwiftMemorySession(Thread owner) { - this.owner = owner; + public ConfinedSwiftMemorySession() { this.state = new AtomicInteger(ACTIVE); this.resources = new ConfinedResourceList(); - - this.arena = Arena.ofConfined(); } - public void checkValid() throws RuntimeException { - if (this.owner != null && this.owner != Thread.currentThread()) { - throw new WrongThreadException("ConfinedSwift arena is confined to %s but was closed from %s!" - .formatted(this.owner, Thread.currentThread())); - } else if (this.state.get() < ACTIVE) { + void checkValid() throws RuntimeException { + if (this.state.get() < ACTIVE) { throw new RuntimeException("SwiftArena is already closed!"); } } @@ -53,35 +45,21 @@ public void close() { checkValid(); // Cleanup all resources - if (this.state.compareAndExchange(ACTIVE, CLOSED) == ACTIVE) { + if (this.state.compareAndSet(ACTIVE, CLOSED)) { this.resources.runCleanup(); } // else, was already closed; do nothing - - this.arena.close(); } @Override public void register(SwiftInstance instance) { checkValid(); - var statusDestroyedFlag = instance.$statusDestroyedFlag(); - Runnable markAsDestroyed = () -> statusDestroyedFlag.set(true); - - var cleanup = new SwiftInstanceCleanup( - instance.$memorySegment(), - instance.$swiftType(), - markAsDestroyed); + SwiftInstanceCleanup cleanup = instance.$createCleanup(); this.resources.add(cleanup); } - @Override - public MemorySegment allocate(long byteSize, long byteAlignment) { - return arena.allocate(byteSize, byteAlignment); - } - static final class ConfinedResourceList implements SwiftResourceList { - // TODO: Could use intrusive linked list to avoid one indirection here - final List resourceCleanups = new LinkedList<>(); + final Queue resourceCleanups = new ConcurrentLinkedQueue<>(); void add(SwiftInstanceCleanup cleanup) { resourceCleanups.add(cleanup); diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/JNISwiftInstance.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/JNISwiftInstance.java new file mode 100644 index 000000000..4e3b8378f --- /dev/null +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/JNISwiftInstance.java @@ -0,0 +1,41 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package org.swift.swiftkit.core; + +public interface JNISwiftInstance extends SwiftInstance { + /** + * Creates a function that will be called when the value should be destroyed. + * This will be code-generated to call a native method to do deinitialization and deallocation. + *

+ * The reason for this "indirection" is that we cannot have static methods on abstract classes, + * and we can't define the destroy method as a member method, because we assume that the wrapper + * has been released, when we destroy. + *

+ * Warning: The function must not capture {@code this}. + * + * @return a function that is called when the value should be destroyed. + */ + Runnable $createDestroyFunction(); + + long $typeMetadataAddress(); + + @Override + default SwiftInstanceCleanup $createCleanup() { + var statusDestroyedFlag = $statusDestroyedFlag(); + Runnable markAsDestroyed = () -> statusDestroyedFlag.set(true); + + return new JNISwiftInstanceCleanup(this.$createDestroyFunction(), markAsDestroyed); + } +} diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/JNISwiftInstanceCleanup.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/JNISwiftInstanceCleanup.java new file mode 100644 index 000000000..08ddcdab0 --- /dev/null +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/JNISwiftInstanceCleanup.java @@ -0,0 +1,31 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package org.swift.swiftkit.core; + +class JNISwiftInstanceCleanup implements SwiftInstanceCleanup { + private final Runnable destroyFunction; + private final Runnable markAsDestroyed; + + public JNISwiftInstanceCleanup(Runnable destroyFunction, Runnable markAsDestroyed) { + this.destroyFunction = destroyFunction; + this.markAsDestroyed = markAsDestroyed; + } + + @Override + public void run() { + markAsDestroyed.run(); + destroyFunction.run(); + } +} diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/NotImplementedError.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/NotImplementedError.java new file mode 100644 index 000000000..6956eaca4 --- /dev/null +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/NotImplementedError.java @@ -0,0 +1,24 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package org.swift.swiftkit.core; + +public class NotImplementedError extends AssertionError { + + private static final long serialVersionUID = 1L; + + public NotImplementedError(String message) { + super(message); + } +} \ No newline at end of file diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/Preconditions.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/Preconditions.java new file mode 100644 index 000000000..bd1e52b07 --- /dev/null +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/Preconditions.java @@ -0,0 +1,153 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +/* + * Copyright (C) 2007 The Guava Authors + * + * 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. + */ + +package org.swift.swiftkit.core; + +import org.swift.swiftkit.core.annotations.Nullable; + +/** + * Collection of convenience functions to check argument preconditions. + *

+ * Partially based on {@code com.google.common.base.Preconditions}. + */ +public final class Preconditions { + private Preconditions() { + } + + public static void checkArgument(boolean expression) { + if (!expression) { + throw new IllegalArgumentException(); + } + } + + public static void checkArgument(boolean expression, @Nullable String format) { + if (!expression) { + throw new IllegalArgumentException(format); + } + } + + public static void checkArgument(boolean expression, @Nullable String format, + @Nullable Object arg1) { + if (!expression) { + throw new IllegalArgumentException(String.format(format, arg1)); + } + } + + public static void checkArgument(boolean expression, @Nullable String format, + @Nullable Object arg1, + @Nullable Object arg2) { + if (!expression) { + throw new IllegalArgumentException(String.format(format, arg1, arg2)); + } + } + + public static T checkNotNull(@Nullable T reference) { + if (reference == null) { + throw new NullPointerException(); + } + + return reference; + } + + public static T checkNotNull(@Nullable T reference, @Nullable String message) { + if (reference == null) { + throw new NullPointerException(message); + } + + return reference; + } + + /* + * All recent hotspots (as of 2009) *really* like to have the natural code + * + * if (guardExpression) { + * throw new BadException(messageExpression); + * } + * + * refactored so that messageExpression is moved to a separate String-returning method. + * + * if (guardExpression) { + * throw new BadException(badMsg(...)); + * } + * + * The alternative natural refactorings into void or Exception-returning methods are much slower. + * This is a big deal - we're talking factors of 2-8 in microbenchmarks, not just 10-20%. (This is + * a hotspot optimizer bug, which should be fixed, but that's a separate, big project). + * + * The coding pattern above is heavily used in java.util, e.g. in ArrayList. There is a + * RangeCheckMicroBenchmark in the JDK that was used to test this. + * + * But the methods in this class want to throw different exceptions, depending on the args, so it + * appears that this pattern is not directly applicable. But we can use the ridiculous, devious + * trick of throwing an exception in the middle of the construction of another exception. Hotspot + * is fine with that. + */ + + /** + * Ensures that {@code index} specifies a valid element in an array, list or string of size + * {@code size}. An element index may range from zero, inclusive, to {@code size}, exclusive. + * + * @param index a user-supplied index identifying an element of an array, list or string + * @param size the size of that array, list or string + * @return the value of {@code index} + * @throws IndexOutOfBoundsException if {@code index} is negative or is not less than {@code size} + * @throws IllegalArgumentException if {@code size} is negative + */ + public static int checkElementIndex(int index, int size) { + return checkElementIndex(index, size, "index"); + } + + /** + * Ensures that {@code index} specifies a valid element in an array, list or string of size + * {@code size}. An element index may range from zero, inclusive, to {@code size}, exclusive. + * + * @param index a user-supplied index identifying an element of an array, list or string + * @param size the size of that array, list or string + * @param desc the text to use to describe this index in an error message + * @return the value of {@code index} + * @throws IndexOutOfBoundsException if {@code index} is negative or is not less than {@code size} + * @throws IllegalArgumentException if {@code size} is negative + */ + public static int checkElementIndex(int index, int size, String desc) { + // Carefully optimized for execution by hotspot (explanatory comment above) + if (index < 0 || index >= size) { + throw new IndexOutOfBoundsException( + String.format("%s, index:%d, size:%d", desc, index, size)); + } + return index; + } + + public static void checkPositionIndexes(int start, int end, int size) { + // Carefully optimized for execution by hotspot (explanatory comment above) + if (start < 0 || end < start || end > size) { + throw new IndexOutOfBoundsException( + String.format("Start index:%d, end index:%d, size: %d", start, end, size)); + } + } + +} diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SimpleCompletableFuture.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SimpleCompletableFuture.java new file mode 100644 index 000000000..b92527236 --- /dev/null +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SimpleCompletableFuture.java @@ -0,0 +1,223 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package org.swift.swiftkit.core; + +import java.util.Deque; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; + +/** + * A simple completable {@link Future} for platforms that do not support {@link java.util.concurrent.CompletableFuture}, + * e.g. before Java 8, and/or before Android 23. + *

+ * Prefer using the {@link CompletableFuture} for bridging Swift asynchronous functions, i.e. use the {@code completableFuture} + * mode in {@code swift-java jextract}. + * + * @param The result type + */ +public final class SimpleCompletableFuture implements Future { + // Marker object used to indicate the Future has not yet been completed. + private static final Object PENDING = new Object(); + private static final Object NULL = new Object(); + private final AtomicReference result = new AtomicReference<>(PENDING); + + private final Deque callbacks = new ConcurrentLinkedDeque<>(); + + /** + * Wrapper type we use to indicate that a recorded result was a failure (recorded using {@link SimpleCompletableFuture#completeExceptionally(Throwable)}. + * Since no-one else can instantiate this type, we know for sure that a recorded CompletedExceptionally indicates a failure. + */ + static final class CompletedExceptionally { + private final Throwable exception; + + private CompletedExceptionally(Throwable exception) { + this.exception = exception; + } + } + + /** + * Returns a new future that, when this stage completes + * normally, is executed with this stage's result as the argument + * to the supplied function. + * + *

This method is analogous to + * {@link java.util.Optional#map Optional.map} and + * {@link java.util.stream.Stream#map Stream.map}. + * + * @return the new Future + */ + public Future thenApply(Function fn) { + SimpleCompletableFuture newFuture = new SimpleCompletableFuture<>(); + addCallback(() -> { + Object observed = this.result.get(); + if (observed instanceof CompletedExceptionally) { + newFuture.completeExceptionally(((CompletedExceptionally) observed).exception); + } else { + try { + // We're guaranteed that an observed result is of type T. + // noinspection unchecked + U newResult = fn.apply(observed == NULL ? null : (T) observed); + newFuture.complete(newResult); + } catch (Throwable t) { + newFuture.completeExceptionally(t); + } + } + }); + return newFuture; + } + + /** + * If not already completed, sets the value returned by {@link #get()} and + * related methods to the given value. + * + * @param value the result value + * @return {@code true} if this invocation caused this CompletableFuture + * to transition to a completed state, else {@code false} + */ + public boolean complete(T value) { + if (result.compareAndSet(PENDING, value == null ? NULL : value)) { + synchronized (result) { + result.notifyAll(); + } + runCallbacks(); + return true; + } + + return false; + } + + /** + * If not already completed, causes invocations of {@link #get()} + * and related methods to throw the given exception. + * + * @param ex the exception + * @return {@code true} if this invocation caused this CompletableFuture + * to transition to a completed state, else {@code false} + */ + public boolean completeExceptionally(Throwable ex) { + if (result.compareAndSet(PENDING, new CompletedExceptionally(ex))) { + synchronized (result) { + result.notifyAll(); + } + runCallbacks(); + return true; + } + + return false; + } + + private void runCallbacks() { + // This is a pretty naive implementation; even if we enter this by racing a thenApply, + // with a completion; we're using a concurrent deque so we won't happen to trigger a callback twice. + Runnable callback; + while ((callback = callbacks.pollFirst()) != null) { + callback.run(); + } + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + // TODO: If we're representing a Swift Task computation with this future, + // we could trigger a Task.cancel() from here + return false; + } + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public boolean isDone() { + return this.result.get() != PENDING; + } + + @Override + public T get() throws InterruptedException, ExecutionException { + Object observed; + // If PENDING check fails immediately, we have no need to take the result lock at all. + while ((observed = result.get()) == PENDING) { + synchronized (result) { + if (result.get() == PENDING) { + result.wait(); + } + } + } + + return getReturn(observed); + } + + @Override + public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + Object observed; + + // Fast path: are we already completed and don't need to do any waiting? + if ((observed = result.get()) != PENDING) { + return get(); + } + + long nanos = unit.toNanos(timeout); + synchronized (result) { + if (!isDone()) { + if (nanos <= 0) { + throw new TimeoutException(); + } + long deadline = System.nanoTime() + nanos; + while (!isDone()) { + nanos = deadline - System.nanoTime(); + if (nanos <= 0L) { + throw new TimeoutException(); + } + result.wait(nanos / 1000000, (int) (nanos % 1000000)); + } + } + } + + // Seems we broke out of the wait loop, let's trigger the 'get()' implementation + observed = result.get(); + if (observed == PENDING) { + throw new ExecutionException("Unexpectedly finished wait-loop while future was not completed, this is a bug.", null); + } + return getReturn(observed); + } + + private T getReturn(Object observed) throws ExecutionException { + if (observed instanceof CompletedExceptionally) { + // We observed a failure, unwrap and throw it + Throwable exception = ((CompletedExceptionally) observed).exception; + if (exception instanceof CancellationException) { + throw (CancellationException) exception; + } + throw new ExecutionException(exception); + } else if (observed == NULL) { + return null; + } else { + // We're guaranteed that we only allowed registering completions of type `T` + // noinspection unchecked + return (T) observed; + } + } + + private void addCallback(Runnable action) { + callbacks.add(action); + if (isDone()) { + // This may race, but we don't care since triggering the callbacks is going to be at-most-once + // by means of using the concurrent deque as our list of callbacks. + runCallbacks(); + } + } + +} \ No newline at end of file diff --git a/SwiftKit/src/main/java/org/swift/swiftkit/SwiftArena.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftArena.java similarity index 68% rename from SwiftKit/src/main/java/org/swift/swiftkit/SwiftArena.java rename to SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftArena.java index f50025cce..96353c375 100644 --- a/SwiftKit/src/main/java/org/swift/swiftkit/SwiftArena.java +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftArena.java @@ -12,9 +12,8 @@ // //===----------------------------------------------------------------------===// -package org.swift.swiftkit; +package org.swift.swiftkit.core; -import java.lang.foreign.SegmentAllocator; import java.util.concurrent.ThreadFactory; /** @@ -24,39 +23,26 @@ *

A confined arena has an associated owner thread that confines some operations to * associated owner thread such as {@link ClosableSwiftArena#close()}. */ -public interface SwiftArena extends SegmentAllocator { +public interface SwiftArena { + /** + * Register a Swift object. + * Its memory should be considered managed by this arena, and be destroyed when the arena is closed. + */ + void register(SwiftInstance instance); static ClosableSwiftArena ofConfined() { - return new ConfinedSwiftMemorySession(Thread.currentThread()); + return new ConfinedSwiftMemorySession(); } static SwiftArena ofAuto() { ThreadFactory cleanerThreadFactory = r -> new Thread(r, "AutoSwiftArenaCleanerThread"); return new AutoSwiftMemorySession(cleanerThreadFactory); } - - /** - * Register a Swift object. - * Its memory should be considered managed by this arena, and be destroyed when the arena is closed. - */ - void register(SwiftInstance instance); - } /** * Represents a list of resources that need a cleanup, e.g. allocated classes/structs. */ interface SwiftResourceList { - void runCleanup(); - -} - - -final class UnexpectedRetainCountException extends RuntimeException { - public UnexpectedRetainCountException(Object resource, long retainCount, int expectedRetainCount) { - super(("Attempting to cleanup managed memory segment %s, but it's retain count was different than [%d] (was %d)! " + - "This would result in destroying a swift object that is still retained by other code somewhere." - ).formatted(resource, expectedRetainCount, retainCount)); - } } diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftInstance.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftInstance.java new file mode 100644 index 000000000..3ab07ffb4 --- /dev/null +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftInstance.java @@ -0,0 +1,56 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package org.swift.swiftkit.core; + +import java.util.concurrent.atomic.AtomicBoolean; + +public interface SwiftInstance { + /** + * Pointer to the {@code self} of the underlying Swift object or value. + * + * @apiNote When using this pointer one must ensure that the underlying object + * is kept alive using some means (e.g. a class remains retained), as + * this function does not ensure safety of the address in any way. + */ + long $memoryAddress(); + + /** + * Called when the arena has decided the value should be destroyed. + *

+ * Warning: The cleanup action must not capture {@code this}. + */ + SwiftInstanceCleanup $createCleanup(); + + /** + * Exposes a boolean value which can be used to indicate if the object was destroyed. + *

+ * This is exposing the object, rather than performing the action because we don't want to accidentally + * form a strong reference to the {@code SwiftInstance} which could prevent the cleanup from running, + * if using an GC managed instance (e.g. using an {@code AutoSwiftMemorySession}. + */ + AtomicBoolean $statusDestroyedFlag(); + /** + * Ensures that this instance has not been destroyed. + *

+ * If this object has been destroyed, calling this method will cause an {@link IllegalStateException} + * to be thrown. This check should be performed before accessing {@code $memorySegment} to prevent + * use-after-free errors. + */ + default void $ensureAlive() { + if (this.$statusDestroyedFlag().get()) { + throw new IllegalStateException("Attempted to call method on already destroyed instance of " + getClass().getSimpleName() + "!"); + } + } +} diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftInstanceCleanup.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftInstanceCleanup.java new file mode 100644 index 000000000..564169687 --- /dev/null +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftInstanceCleanup.java @@ -0,0 +1,20 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package org.swift.swiftkit.core; + +/** + * A Swift memory instance cleanup, e.g. count-down a reference count and destroy a class, or destroy struct/enum etc. + */ +public interface SwiftInstanceCleanup extends Runnable {} \ No newline at end of file diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftLibraries.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftLibraries.java new file mode 100644 index 000000000..7eccde749 --- /dev/null +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftLibraries.java @@ -0,0 +1,81 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package org.swift.swiftkit.core; + +import org.swift.swiftkit.core.util.PlatformUtils; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; + +public final class SwiftLibraries { + + // Library names of core Swift and SwiftRuntimeFunctions + public static final String LIB_NAME_SWIFT_CORE = "swiftCore"; + public static final String LIB_NAME_SWIFT_CONCURRENCY = "swift_Concurrency"; + public static final String LIB_NAME_SWIFT_RUNTIME_FUNCTIONS = "SwiftRuntimeFunctions"; + + /** + * Allows for configuration if jextracted types should automatically attempt to load swiftCore and the library type is from. + *

+ * If all libraries you need to load are available in paths passed to {@code -Djava.library.path} this should work correctly, + * however if attempting to load libraries from e.g. the jar as a resource, you may want to disable this. + */ + public static final boolean AUTO_LOAD_LIBS = System.getProperty("swift-java.auto-load-libraries") == null ? + true + : Boolean.getBoolean("swiftkit.auto-load-libraries"); + + @SuppressWarnings("unused") + private static final boolean INITIALIZED_LIBS = loadLibraries(false); + + public static boolean loadLibraries(boolean loadSwiftRuntimeFunctions) { + System.loadLibrary(LIB_NAME_SWIFT_CORE); + if (loadSwiftRuntimeFunctions) { + System.loadLibrary(LIB_NAME_SWIFT_RUNTIME_FUNCTIONS); + } + return true; + } + + // ==== ------------------------------------------------------------------------------------------------------------ + // Loading libraries + + public static void loadResourceLibrary(String libname) { + String resourceName = PlatformUtils.dynamicLibraryName(libname); + if (CallTraces.TRACE_DOWNCALLS) { + System.out.println("[swift-java] Loading resource library: " + resourceName); + } + + try (InputStream libInputStream = SwiftLibraries.class.getResourceAsStream("/" + resourceName)) { + if (libInputStream == null) { + throw new RuntimeException("Expected library '" + libname + "' ('" + resourceName + "') was not found as resource!"); + } + + // TODO: we could do an in memory file system here + File tempFile = File.createTempFile(libname, ""); + tempFile.deleteOnExit(); + Files.copy(libInputStream, tempFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + + System.load(tempFile.getAbsolutePath()); + } catch (IOException e) { + throw new RuntimeException("Failed to load dynamic library '" + libname + "' ('" + resourceName + "') as resource!", e); + } + } + + public static String getJavaLibraryPath() { + return System.getProperty("java.library.path"); + } +} diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftMemoryManagement.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftMemoryManagement.java new file mode 100644 index 000000000..2b9a12092 --- /dev/null +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftMemoryManagement.java @@ -0,0 +1,19 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package org.swift.swiftkit.core; + +public class SwiftMemoryManagement { + public static final SwiftArena GLOBAL_SWIFT_JAVA_ARENA = SwiftArena.ofAuto(); +} diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftObjects.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftObjects.java new file mode 100644 index 000000000..c508e90e4 --- /dev/null +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftObjects.java @@ -0,0 +1,26 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package org.swift.swiftkit.core; + +/** + * Utility functions, similar to @{link java.util.Objects} + */ +public class SwiftObjects { + public static void requireNonZero(long number, String name) { + if (number == 0) { + throw new IllegalArgumentException(String.format("'%s' must not be zero!", name)); + } + } +} diff --git a/SwiftKit/src/main/java/org/swift/swiftkit/SwiftValue.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftValue.java similarity index 94% rename from SwiftKit/src/main/java/org/swift/swiftkit/SwiftValue.java rename to SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftValue.java index a364c0129..3e9c1aff0 100644 --- a/SwiftKit/src/main/java/org/swift/swiftkit/SwiftValue.java +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftValue.java @@ -12,7 +12,7 @@ // //===----------------------------------------------------------------------===// -package org.swift.swiftkit; +package org.swift.swiftkit.core; /** * Represent a wrapper around a Swift value object. e.g. {@code struct} or {@code enum}. diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/WrongThreadException.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/WrongThreadException.java new file mode 100644 index 000000000..d15436f95 --- /dev/null +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/WrongThreadException.java @@ -0,0 +1,21 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package org.swift.swiftkit.core; + +public class WrongThreadException extends RuntimeException { + public WrongThreadException(String message) { + super(message); + } +} diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/annotations/NonNull.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/annotations/NonNull.java new file mode 100644 index 000000000..cad6cd8bf --- /dev/null +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/annotations/NonNull.java @@ -0,0 +1,28 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package org.swift.swiftkit.core.annotations; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +// TODO: Consider depending on jspecify instead +@Documented +@Target(TYPE_USE) +@Retention(RUNTIME) +public @interface NonNull {} \ No newline at end of file diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/annotations/Nullable.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/annotations/Nullable.java new file mode 100644 index 000000000..c20ad884c --- /dev/null +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/annotations/Nullable.java @@ -0,0 +1,28 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package org.swift.swiftkit.core.annotations; + +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +// TODO: Consider depending on jspecify instead +@Documented +@Target(TYPE_USE) +@Retention(RUNTIME) +public @interface Nullable {} \ No newline at end of file diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/annotations/ThreadSafe.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/annotations/ThreadSafe.java new file mode 100644 index 000000000..2e62a8b66 --- /dev/null +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/annotations/ThreadSafe.java @@ -0,0 +1,42 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package org.swift.swiftkit.core.annotations; + +import jdk.jfr.Description; +import jdk.jfr.Label; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; + +/** + * Used to mark a type as thread-safe, i.e. no additional synchronization is necessary when accessing it + * from multiple threads. + * + *

In SwiftJava specifically, this attribute is applied when an extracted Swift type conforms to the Swift + * {@code Sendable} protocol, which is a compiler enforced mechanism to enforce thread-safety in Swift. + * + * @see Swift Sendable API documentation. + */ +@Documented +@Label("Thread-safe") +@Description("Value should be interpreted as safe to be shared across threads.") +@Target({TYPE_USE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ThreadSafe { +} diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/annotations/Unsigned.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/annotations/Unsigned.java new file mode 100644 index 000000000..4bf8e3544 --- /dev/null +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/annotations/Unsigned.java @@ -0,0 +1,42 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package org.swift.swiftkit.core.annotations; + +import jdk.jfr.Description; +import jdk.jfr.Label; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; + +/** + * Value is of an unsigned numeric type. + *

+ * This annotation is used to annotate java integer primitives when their + * corresponding Swift type was actually unsigned, e.g. an {@code @Unsigned long} + * in a method signature corresponds to a Swift {@code UInt64} type, and therefore + * negative values reported by the signed {@code long} should instead be interpreted positive values, + * larger than {@code Long.MAX_VALUE} that are just not representable using a signed {@code long}. + */ +@Documented +@Label("Unsigned integer type") +@Description("Value should be interpreted as unsigned data type") +@Target({TYPE_USE, PARAMETER, FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Unsigned { +} diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/ref/PhantomCleanable.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/ref/PhantomCleanable.java new file mode 100644 index 000000000..2efcdae7d --- /dev/null +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/ref/PhantomCleanable.java @@ -0,0 +1,35 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package org.swift.swiftkit.core.ref; + +import java.lang.ref.PhantomReference; + +public class PhantomCleanable extends PhantomReference { + private final Runnable cleanupAction; + private final SwiftCleaner swiftCleaner; + + public PhantomCleanable(Object referent, SwiftCleaner swiftCleaner, Runnable cleanupAction) { + super(referent, swiftCleaner.referenceQueue); + this.cleanupAction = cleanupAction; + this.swiftCleaner = swiftCleaner; + swiftCleaner.list.add(this); + } + + public void cleanup() { + if (swiftCleaner.list.remove(this)) { + cleanupAction.run(); + } + } +} diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/ref/SwiftCleaner.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/ref/SwiftCleaner.java new file mode 100644 index 000000000..2a5b49f5e --- /dev/null +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/ref/SwiftCleaner.java @@ -0,0 +1,67 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package org.swift.swiftkit.core.ref; + +import java.lang.ref.ReferenceQueue; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ThreadFactory; + +public class SwiftCleaner implements Runnable { + final ReferenceQueue referenceQueue; + final List list; + + private SwiftCleaner() { + this.referenceQueue = new ReferenceQueue<>(); + this.list = Collections.synchronizedList(new LinkedList<>()); + } + + public static SwiftCleaner create(ThreadFactory threadFactory) { + SwiftCleaner swiftCleaner = new SwiftCleaner(); + swiftCleaner.start(threadFactory); + return swiftCleaner; + } + + void start(ThreadFactory threadFactory) { + // This makes sure the linked list is not empty when the thread starts, + // and the thread will run at least until the cleaner itself can be GCed. + new PhantomCleanable(this, this, () -> {}); + + Thread thread = threadFactory.newThread(this); + thread.setDaemon(true); + thread.start(); + } + + public void register(Object resourceHolder, Runnable cleaningAction) { + Objects.requireNonNull(resourceHolder, "resourceHolder"); + Objects.requireNonNull(cleaningAction, "cleaningAction"); + new PhantomCleanable(resourceHolder, this, cleaningAction); + } + + @Override + public void run() { + while (!list.isEmpty()) { + try { + PhantomCleanable removed = (PhantomCleanable) referenceQueue.remove(60 * 1000L); + removed.cleanup(); + } catch (Throwable e) { + // ignore exceptions from the cleanup action + // (including interruption of cleanup thread) + } + } + } +} diff --git a/SwiftKit/src/main/java/org/swift/swiftkit/util/PlatformUtils.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/util/PlatformUtils.java similarity index 97% rename from SwiftKit/src/main/java/org/swift/swiftkit/util/PlatformUtils.java rename to SwiftKitCore/src/main/java/org/swift/swiftkit/core/util/PlatformUtils.java index 6addb31de..667bd53b0 100644 --- a/SwiftKit/src/main/java/org/swift/swiftkit/util/PlatformUtils.java +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/util/PlatformUtils.java @@ -12,7 +12,7 @@ // //===----------------------------------------------------------------------===// -package org.swift.swiftkit.util; +package org.swift.swiftkit.core.util; public class PlatformUtils { public static boolean isLinux() { diff --git a/SwiftKit/src/main/java/org/swift/swiftkit/util/StringUtils.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/util/StringUtils.java similarity index 96% rename from SwiftKit/src/main/java/org/swift/swiftkit/util/StringUtils.java rename to SwiftKitCore/src/main/java/org/swift/swiftkit/core/util/StringUtils.java index 6753ea73a..1ce4259c5 100644 --- a/SwiftKit/src/main/java/org/swift/swiftkit/util/StringUtils.java +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/util/StringUtils.java @@ -12,7 +12,7 @@ // //===----------------------------------------------------------------------===// -package org.swift.swiftkit.util; +package org.swift.swiftkit.core.util; public class StringUtils { public static String stripPrefix(String mangledName, String prefix) { diff --git a/SwiftKitCore/src/test/java/org/swift/swiftkit/AutoArenaTest.java b/SwiftKitCore/src/test/java/org/swift/swiftkit/AutoArenaTest.java new file mode 100644 index 000000000..120934215 --- /dev/null +++ b/SwiftKitCore/src/test/java/org/swift/swiftkit/AutoArenaTest.java @@ -0,0 +1,76 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package org.swift.swiftkit; + +import org.junit.jupiter.api.Test; +import org.swift.swiftkit.core.JNISwiftInstance; +import org.swift.swiftkit.core.SwiftArena; + +import java.util.concurrent.atomic.AtomicBoolean; + +public class AutoArenaTest { + + @Test + @SuppressWarnings("removal") // System.runFinalization() will be removed + public void cleaner_releases_native_resource() { + SwiftArena arena = SwiftArena.ofAuto(); + + // This object is registered to the arena. + var object = new FakeSwiftInstance(arena); + var statusDestroyedFlag = object.$statusDestroyedFlag(); + + // Release the object and hope it gets GC-ed soon + + // noinspection UnusedAssignment + object = null; + + var i = 1_000; + while (!statusDestroyedFlag.get()) { + System.runFinalization(); + System.gc(); + + if (i-- < 1) { + throw new RuntimeException("Reference was not cleaned up! Did Cleaner not pick up the release?"); + } + } + } + + private static class FakeSwiftInstance implements JNISwiftInstance { + AtomicBoolean $state$destroyed = new AtomicBoolean(false); + + public FakeSwiftInstance(SwiftArena arena) { + arena.register(this); + } + + public Runnable $createDestroyFunction() { + return () -> {}; + } + + @Override + public long $typeMetadataAddress() { + return 0; + } + + @Override + public long $memoryAddress() { + return 0; + } + + @Override + public AtomicBoolean $statusDestroyedFlag() { + return $state$destroyed; + } + } +} diff --git a/SwiftKitCore/src/test/java/org/swift/swiftkit/core/SimpleCompletableFutureTest.java b/SwiftKitCore/src/test/java/org/swift/swiftkit/core/SimpleCompletableFutureTest.java new file mode 100644 index 000000000..b4bb98b3a --- /dev/null +++ b/SwiftKitCore/src/test/java/org/swift/swiftkit/core/SimpleCompletableFutureTest.java @@ -0,0 +1,185 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package org.swift.swiftkit.core; + +import org.junit.jupiter.api.Test; + +import java.util.Objects; +import java.util.concurrent.*; +import static org.junit.jupiter.api.Assertions.*; + +public class SimpleCompletableFutureTest { + + @Test + void testCompleteAndGet() throws ExecutionException, InterruptedException { + SimpleCompletableFuture future = new SimpleCompletableFuture<>(); + assertFalse(future.isDone()); + assertTrue(future.complete("test")); + assertTrue(future.isDone()); + assertEquals("test", future.get()); + } + + @Test + void testCompleteWithNullAndGet() throws ExecutionException, InterruptedException { + SimpleCompletableFuture future = new SimpleCompletableFuture<>(); + assertFalse(future.isDone()); + assertTrue(future.complete(null)); + assertTrue(future.isDone()); + assertNull(future.get()); + } + + @Test + void testCompleteExceptionallyAndGet() { + SimpleCompletableFuture future = new SimpleCompletableFuture<>(); + RuntimeException ex = new RuntimeException("Test Exception"); + assertTrue(future.completeExceptionally(ex)); + assertTrue(future.isDone()); + + ExecutionException thrown = assertThrows(ExecutionException.class, future::get); + assertEquals(ex, thrown.getCause()); + } + + @Test + void testGetWithTimeout_timesOut() { + SimpleCompletableFuture future = new SimpleCompletableFuture<>(); + assertThrows(TimeoutException.class, () -> future.get(10, TimeUnit.MILLISECONDS)); + } + + @Test + void testGetWithTimeout_completesInTime() throws ExecutionException, InterruptedException, TimeoutException { + SimpleCompletableFuture future = new SimpleCompletableFuture<>(); + future.complete("fast"); + assertEquals("fast", future.get(10, TimeUnit.MILLISECONDS)); + } + + @Test + void testGetWithTimeout_completesInTimeAfterWait() throws Exception { + SimpleCompletableFuture future = new SimpleCompletableFuture<>(); + Thread t = new Thread(() -> { + try { + Thread.sleep(50); + } catch (InterruptedException e) { + // ignore + } + future.complete("late"); + }); + t.start(); + assertEquals("late", future.get(200, TimeUnit.MILLISECONDS)); + } + + @Test + void testThenApply() throws ExecutionException, InterruptedException { + SimpleCompletableFuture future = new SimpleCompletableFuture<>(); + Future mapped = future.thenApply(String::length); + + future.complete("hello"); + + assertEquals(5, mapped.get()); + } + + @Test + void testThenApplyOnCompletedFuture() throws ExecutionException, InterruptedException { + SimpleCompletableFuture future = new SimpleCompletableFuture<>(); + future.complete("done"); + + Future mapped = future.thenApply(String::length); + + assertEquals(4, mapped.get()); + } + + @Test + void testThenApplyWithNull() throws ExecutionException, InterruptedException { + SimpleCompletableFuture future = new SimpleCompletableFuture<>(); + Future mapped = future.thenApply(Objects::isNull); + + future.complete(null); + + assertTrue(mapped.get()); + } + + @Test + void testThenApplyExceptionally() { + SimpleCompletableFuture future = new SimpleCompletableFuture<>(); + RuntimeException ex = new RuntimeException("Initial Exception"); + Future mapped = future.thenApply(String::length); + + future.completeExceptionally(ex); + + ExecutionException thrown = assertThrows(ExecutionException.class, mapped::get); + assertEquals(ex, thrown.getCause()); + } + + @Test + void testThenApplyTransformationThrows() { + SimpleCompletableFuture future = new SimpleCompletableFuture<>(); + RuntimeException ex = new RuntimeException("Transformation Exception"); + Future mapped = future.thenApply(s -> { + throw ex; + }); + + future.complete("hello"); + + ExecutionException thrown = assertThrows(ExecutionException.class, mapped::get); + assertEquals(ex, thrown.getCause()); + } + + @Test + void testCompleteTwice() throws ExecutionException, InterruptedException { + SimpleCompletableFuture future = new SimpleCompletableFuture<>(); + + assertTrue(future.complete("first")); + assertFalse(future.complete("second")); + + assertEquals("first", future.get()); + } + + @Test + void testCompleteThenCompleteExceptionally() throws ExecutionException, InterruptedException { + SimpleCompletableFuture future = new SimpleCompletableFuture<>(); + + assertTrue(future.complete("first")); + assertFalse(future.completeExceptionally(new RuntimeException("second"))); + + assertEquals("first", future.get()); + } + + @Test + void testCompleteExceptionallyThenComplete() { + SimpleCompletableFuture future = new SimpleCompletableFuture<>(); + RuntimeException ex = new RuntimeException("first"); + + assertTrue(future.completeExceptionally(ex)); + assertFalse(future.complete("second")); + + ExecutionException thrown = assertThrows(ExecutionException.class, future::get); + assertEquals(ex, thrown.getCause()); + } + + @Test + void testIsDone() { + SimpleCompletableFuture future = new SimpleCompletableFuture<>(); + assertFalse(future.isDone()); + future.complete("done"); + assertTrue(future.isDone()); + } + + @Test + void testIsDoneExceptionally() { + SimpleCompletableFuture future = new SimpleCompletableFuture<>(); + assertFalse(future.isDone()); + future.completeExceptionally(new RuntimeException()); + assertTrue(future.isDone()); + } +} diff --git a/SwiftKitFFM/build.gradle b/SwiftKitFFM/build.gradle new file mode 100644 index 000000000..08a36a3e3 --- /dev/null +++ b/SwiftKitFFM/build.gradle @@ -0,0 +1,106 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +plugins { + id("build-logic.java-application-conventions") + id("maven-publish") +} + +group = "org.swift.swiftkit" +version = "1.0-SNAPSHOT" +base { + archivesName = "swiftkit-ffm" +} + +repositories { + mavenLocal() + mavenCentral() +} + +publishing { + publications { + maven(MavenPublication) { + groupId = group + artifactId = 'swiftkit-ffm' + version = '1.0-SNAPSHOT' + + from components.java + } + } +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(25)) + } +} + +dependencies { + implementation(project(':SwiftKitCore')) + + testRuntimeOnly("org.junit.platform:junit-platform-launcher") // TODO: workaround for not finding junit: https://github.com/gradle/gradle/issues/34512 + testImplementation(platform("org.junit:junit-bom:5.10.0")) + testImplementation("org.junit.jupiter:junit-jupiter") +} + +testing { + suites { + test { + useJUnitJupiter('5.10.3') + } + } +} + +tasks.test { + useJUnitPlatform() + testLogging { + events("passed", "skipped", "failed") + } +} + +/// Enable access to preview APIs, e.g. java.lang.foreign.* (Panama) +tasks.withType(JavaCompile).configureEach { + options.compilerArgs.add("--enable-preview") + options.compilerArgs.add("-Xlint:preview") +} + +// SwiftKit depends on SwiftRuntimeFunctions (Swift library that this Java library calls into) + +def compileSwift = tasks.register("compileSwift", Exec) { + description "Compile the swift-java SwiftRuntimeFunctions dynamic library that SwiftKit (Java) calls into" + + inputs.file(new File(rootDir, "Package.swift")) + inputs.dir(new File(rootDir, "Sources/")) // a bit generous, but better safe than sorry, and include all Swift source changes + outputs.dir(new File(rootDir, ".build")) + + workingDir = rootDir + commandLine "swift" + // FIXME: disable prebuilts until swift-syntax isn't broken on 6.2 anymore: https://github.com/swiftlang/swift-java/issues/418 + args("build", "--disable-experimental-prebuilts", "--target", "SwiftRuntimeFunctions") +} +tasks.build { + dependsOn("compileSwift") +} + + + +def cleanSwift = tasks.register("cleanSwift", Exec) { + workingDir = rootDir + commandLine "swift" + args("package", "clean") +} +tasks.clean { + dependsOn("cleanSwift") +} + diff --git a/SwiftKit/src/main/java/org/swift/swiftkit/AutoSwiftMemorySession.java b/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/AllocatingAutoSwiftMemorySession.java similarity index 78% rename from SwiftKit/src/main/java/org/swift/swiftkit/AutoSwiftMemorySession.java rename to SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/AllocatingAutoSwiftMemorySession.java index ecbe836ef..7063fefb6 100644 --- a/SwiftKit/src/main/java/org/swift/swiftkit/AutoSwiftMemorySession.java +++ b/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/AllocatingAutoSwiftMemorySession.java @@ -12,7 +12,10 @@ // //===----------------------------------------------------------------------===// -package org.swift.swiftkit; +package org.swift.swiftkit.ffm; + +import org.swift.swiftkit.core.SwiftArena; +import org.swift.swiftkit.core.SwiftInstance; import java.lang.foreign.Arena; import java.lang.foreign.MemorySegment; @@ -26,7 +29,7 @@ *

When registered Java wrapper classes around native Swift instances {@link SwiftInstance}, * are eligible for collection, this will trigger the cleanup of the native resources as well. * - *

This memory session is LESS reliable than using a {@link ConfinedSwiftMemorySession} because + *

This memory session is LESS reliable than using a {@link FFMConfinedSwiftMemorySession} because * the timing of when the native resources are cleaned up is somewhat undefined, and rely on the * system GC. Meaning, that if an object nas been promoted to an old generation, there may be a * long time between the resource no longer being referenced "in Java" and its native memory being released, @@ -37,12 +40,12 @@ * *

Whenever possible, prefer using an explicitly managed {@link SwiftArena}, such as {@link SwiftArena#ofConfined()}. */ -final class AutoSwiftMemorySession implements SwiftArena { +final class AllocatingAutoSwiftMemorySession implements AllocatingSwiftArena { private final Arena arena; private final Cleaner cleaner; - public AutoSwiftMemorySession(ThreadFactory cleanerThreadFactory) { + public AllocatingAutoSwiftMemorySession(ThreadFactory cleanerThreadFactory) { this.cleaner = Cleaner.create(cleanerThreadFactory); this.arena = Arena.ofAuto(); } @@ -51,12 +54,9 @@ public AutoSwiftMemorySession(ThreadFactory cleanerThreadFactory) { public void register(SwiftInstance instance) { Objects.requireNonNull(instance, "value"); - // We're doing this dance to avoid keeping a strong reference to the value itself - var statusDestroyedFlag = instance.$statusDestroyedFlag(); - Runnable markAsDestroyed = () -> statusDestroyedFlag.set(true); - - MemorySegment resource = instance.$memorySegment(); - var cleanupAction = new SwiftInstanceCleanup(resource, instance.$swiftType(), markAsDestroyed); + // We make sure we don't capture `instance` in the + // cleanup action, so we can ignore the warning below. + var cleanupAction = instance.$createCleanup(); cleaner.register(instance, cleanupAction); } @@ -64,4 +64,4 @@ public void register(SwiftInstance instance) { public MemorySegment allocate(long byteSize, long byteAlignment) { return arena.allocate(byteSize, byteAlignment); } -} +} \ No newline at end of file diff --git a/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/AllocatingSwiftArena.java b/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/AllocatingSwiftArena.java new file mode 100644 index 000000000..b72818a3e --- /dev/null +++ b/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/AllocatingSwiftArena.java @@ -0,0 +1,34 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package org.swift.swiftkit.ffm; + +import org.swift.swiftkit.core.SwiftArena; + +import java.lang.foreign.MemorySegment; +import java.lang.foreign.SegmentAllocator; +import java.util.concurrent.ThreadFactory; + +public interface AllocatingSwiftArena extends SwiftArena, SegmentAllocator { + MemorySegment allocate(long byteSize, long byteAlignment); + + static ClosableAllocatingSwiftArena ofConfined() { + return new FFMConfinedSwiftMemorySession(); + } + + static AllocatingSwiftArena ofAuto() { + ThreadFactory cleanerThreadFactory = r -> new Thread(r, "AutoSwiftArenaCleanerThread"); + return new AllocatingAutoSwiftMemorySession(cleanerThreadFactory); + } +} diff --git a/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/ClosableAllocatingSwiftArena.java b/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/ClosableAllocatingSwiftArena.java new file mode 100644 index 000000000..70cdcbb6f --- /dev/null +++ b/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/ClosableAllocatingSwiftArena.java @@ -0,0 +1,22 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package org.swift.swiftkit.ffm; + +import org.swift.swiftkit.core.ClosableSwiftArena; + +/** + * Auto-closable version of {@link AllocatingSwiftArena}. + */ +public interface ClosableAllocatingSwiftArena extends ClosableSwiftArena, AllocatingSwiftArena {} diff --git a/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/FFMConfinedSwiftMemorySession.java b/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/FFMConfinedSwiftMemorySession.java new file mode 100644 index 000000000..05daebd7a --- /dev/null +++ b/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/FFMConfinedSwiftMemorySession.java @@ -0,0 +1,40 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package org.swift.swiftkit.ffm; + +import org.swift.swiftkit.core.ConfinedSwiftMemorySession; + +import java.lang.foreign.Arena; +import java.lang.foreign.MemorySegment; + +final class FFMConfinedSwiftMemorySession extends ConfinedSwiftMemorySession implements AllocatingSwiftArena, ClosableAllocatingSwiftArena { + final Arena arena; + + public FFMConfinedSwiftMemorySession() { + super(); + this.arena = Arena.ofConfined(); + } + + @Override + public void close() { + super.close(); + this.arena.close(); + } + + @Override + public MemorySegment allocate(long byteSize, long byteAlignment) { + return arena.allocate(byteSize, byteAlignment); + } +} diff --git a/SwiftKit/src/main/java/org/swift/swiftkit/SwiftInstance.java b/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/FFMSwiftInstance.java similarity index 65% rename from SwiftKit/src/main/java/org/swift/swiftkit/SwiftInstance.java rename to SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/FFMSwiftInstance.java index 5b8ff7007..70324c942 100644 --- a/SwiftKit/src/main/java/org/swift/swiftkit/SwiftInstance.java +++ b/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/FFMSwiftInstance.java @@ -12,43 +12,45 @@ // //===----------------------------------------------------------------------===// -package org.swift.swiftkit; +package org.swift.swiftkit.ffm; + +import org.swift.swiftkit.core.SwiftInstance; +import org.swift.swiftkit.core.SwiftInstanceCleanup; -import java.lang.foreign.GroupLayout; import java.lang.foreign.MemorySegment; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Supplier; - -public abstract class SwiftInstance { - /// Pointer to the "self". - private final MemorySegment selfMemorySegment; - /** - * The pointer to the instance in memory. I.e. the {@code self} of the Swift object or value. - */ - public final MemorySegment $memorySegment() { - return this.selfMemorySegment; - } +public abstract class FFMSwiftInstance implements SwiftInstance { + private final MemorySegment memorySegment; // TODO: make this a flagset integer and/or use a field updater /** Used to track additional state of the underlying object, e.g. if it was explicitly destroyed. */ private final AtomicBoolean $state$destroyed = new AtomicBoolean(false); /** - * Exposes a boolean value which can be used to indicate if the object was destroyed. - *

- * This is exposing the object, rather than performing the action because we don't want to accidentally - * form a strong reference to the {@code SwiftInstance} which could prevent the cleanup from running, - * if using an GC managed instance (e.g. using an {@link AutoSwiftMemorySession}. + * The designated constructor of any imported Swift types. + * + * @param segment the memory segment. + * @param arena the arena this object belongs to. When the arena goes out of scope, this value is destroyed. */ - public final AtomicBoolean $statusDestroyedFlag() { - return this.$state$destroyed; + protected FFMSwiftInstance(MemorySegment segment, AllocatingSwiftArena arena) { + this.memorySegment = segment; + + // Only register once we have fully initialized the object since this will need the object pointer. + arena.register(this); } /** - * The in memory layout of an instance of this Swift type. + * The pointer to the instance in memory. I.e. the {@code self} of the Swift object or value. */ - public abstract GroupLayout $layout(); + public final MemorySegment $memorySegment() { + return this.memorySegment; + } + + @Override + public long $memoryAddress() { + return $memorySegment().address(); + } /** * The Swift type metadata of this type. @@ -56,27 +58,26 @@ public abstract class SwiftInstance { public abstract SwiftAnyType $swiftType(); /** - * The designated constructor of any imported Swift types. - * - * @param segment the memory segment. - * @param arena the arena this object belongs to. When the arena goes out of scope, this value is destroyed. + * Exposes a boolean value which can be used to indicate if the object was destroyed. + *

+ * This is exposing the object, rather than performing the action because we don't want to accidentally + * form a strong reference to the {@code SwiftInstance} which could prevent the cleanup from running, + * if using an GC managed instance (e.g. using an {@code AutoSwiftMemorySession}. */ - protected SwiftInstance(MemorySegment segment, SwiftArena arena) { - this.selfMemorySegment = segment; - arena.register(this); + public final AtomicBoolean $statusDestroyedFlag() { + return this.$state$destroyed; } - /** - * Ensures that this instance has not been destroyed. - *

- * If this object has been destroyed, calling this method will cause an {@link IllegalStateException} - * to be thrown. This check should be performed before accessing {@code $memorySegment} to prevent - * use-after-free errors. - */ - protected final void $ensureAlive() { - if (this.$state$destroyed.get()) { - throw new IllegalStateException("Attempted to call method on already destroyed instance of " + getClass().getSimpleName() + "!"); - } + @Override + public SwiftInstanceCleanup $createCleanup() { + var statusDestroyedFlag = $statusDestroyedFlag(); + Runnable markAsDestroyed = () -> statusDestroyedFlag.set(true); + + return new FFMSwiftInstanceCleanup( + $memorySegment(), + $swiftType(), + markAsDestroyed + ); } /** diff --git a/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/FFMSwiftInstanceCleanup.java b/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/FFMSwiftInstanceCleanup.java new file mode 100644 index 000000000..7b7bb1ec8 --- /dev/null +++ b/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/FFMSwiftInstanceCleanup.java @@ -0,0 +1,44 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package org.swift.swiftkit.ffm; + +import org.swift.swiftkit.core.SwiftInstanceCleanup; + +import static org.swift.swiftkit.ffm.SwiftJavaLogGroup.LIFECYCLE; + +import java.lang.foreign.MemorySegment; + +public class FFMSwiftInstanceCleanup implements SwiftInstanceCleanup { + private final MemorySegment memoryAddress; + private final SwiftAnyType type; + private final Runnable markAsDestroyed; + + public FFMSwiftInstanceCleanup(MemorySegment memoryAddress, SwiftAnyType type, Runnable markAsDestroyed) { + this.memoryAddress = memoryAddress; + this.type = type; + this.markAsDestroyed = markAsDestroyed; + } + + @Override + public void run() { + markAsDestroyed.run(); + + // Allow null pointers just for AutoArena tests. + if (type != null && memoryAddress != null) { + SwiftRuntime.log(LIFECYCLE, "Destroy swift value [" + type.getSwiftName() + "]: " + memoryAddress); + SwiftValueWitnessTable.destroy(type, memoryAddress); + } + } +} diff --git a/SwiftKit/src/main/java/org/swift/swiftkit/MemorySegmentUtils.java b/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/MemorySegmentUtils.java similarity index 97% rename from SwiftKit/src/main/java/org/swift/swiftkit/MemorySegmentUtils.java rename to SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/MemorySegmentUtils.java index 660a55008..0890b1d8f 100644 --- a/SwiftKit/src/main/java/org/swift/swiftkit/MemorySegmentUtils.java +++ b/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/MemorySegmentUtils.java @@ -12,7 +12,7 @@ // //===----------------------------------------------------------------------===// -package org.swift.swiftkit; +package org.swift.swiftkit.ffm; import java.lang.foreign.MemorySegment; import java.lang.foreign.ValueLayout; diff --git a/SwiftKit/src/main/java/org/swift/swiftkit/SwiftAnyType.java b/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/SwiftAnyType.java similarity index 89% rename from SwiftKit/src/main/java/org/swift/swiftkit/SwiftAnyType.java rename to SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/SwiftAnyType.java index 4d13ecb77..3915725b8 100644 --- a/SwiftKit/src/main/java/org/swift/swiftkit/SwiftAnyType.java +++ b/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/SwiftAnyType.java @@ -12,7 +12,10 @@ // //===----------------------------------------------------------------------===// -package org.swift.swiftkit; +package org.swift.swiftkit.ffm; + +import org.swift.swiftkit.ffm.SwiftRuntime; +import org.swift.swiftkit.ffm.SwiftValueLayout; import java.lang.foreign.GroupLayout; import java.lang.foreign.MemoryLayout; @@ -46,7 +49,7 @@ public SwiftAnyType(MemorySegment memorySegment) { * Get the human-readable Swift type name of this type. */ public String getSwiftName() { - return SwiftKit.nameOfSwiftType(memorySegment, true); + return SwiftRuntime.nameOfSwiftType(memorySegment, true); } @Override diff --git a/SwiftKit/src/main/java/org/swift/swiftkit/SwiftHeapObject.java b/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/SwiftHeapObject.java similarity index 91% rename from SwiftKit/src/main/java/org/swift/swiftkit/SwiftHeapObject.java rename to SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/SwiftHeapObject.java index 89050fb57..b506be90e 100644 --- a/SwiftKit/src/main/java/org/swift/swiftkit/SwiftHeapObject.java +++ b/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/SwiftHeapObject.java @@ -12,7 +12,8 @@ // //===----------------------------------------------------------------------===// -package org.swift.swiftkit; +package org.swift.swiftkit.ffm; + import java.lang.foreign.MemorySegment; @@ -25,7 +26,7 @@ public interface SwiftHeapObject { /** * Pointer to the instance. */ - public default MemorySegment $instance() { + default MemorySegment $instance() { return this.$memorySegment().get(SwiftValueLayout.SWIFT_POINTER, 0); } } diff --git a/SwiftKit/src/main/java/org/swift/swiftkit/SwiftKit.java b/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/SwiftRuntime.java similarity index 80% rename from SwiftKit/src/main/java/org/swift/swiftkit/SwiftKit.java rename to SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/SwiftRuntime.java index 9c04eded9..74a270c0a 100644 --- a/SwiftKit/src/main/java/org/swift/swiftkit/SwiftKit.java +++ b/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/SwiftRuntime.java @@ -12,32 +12,28 @@ // //===----------------------------------------------------------------------===// -package org.swift.swiftkit; +package org.swift.swiftkit.ffm; -import org.swift.swiftkit.util.PlatformUtils; +import org.swift.swiftkit.core.SwiftInstance; +import org.swift.swiftkit.core.CallTraces; +import org.swift.swiftkit.core.util.PlatformUtils; +import org.swift.swiftkit.ffm.SwiftRuntime.swiftjava; -import java.io.File; -import java.io.IOException; import java.lang.foreign.*; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.VarHandle; -import java.nio.file.CopyOption; -import java.nio.file.FileSystems; -import java.nio.file.Files; -import java.nio.file.StandardCopyOption; -import java.util.Arrays; -import java.util.Optional; +import java.util.*; import java.util.stream.Collectors; -import static org.swift.swiftkit.util.StringUtils.stripPrefix; -import static org.swift.swiftkit.util.StringUtils.stripSuffix; +import static org.swift.swiftkit.core.CallTraces.traceDowncall; +import static org.swift.swiftkit.core.util.StringUtils.stripPrefix; +import static org.swift.swiftkit.core.util.StringUtils.stripSuffix; -public class SwiftKit { +public class SwiftRuntime { public static final String STDLIB_DYLIB_NAME = "swiftCore"; - public static final String SWIFTKITSWIFT_DYLIB_NAME = "SwiftKitSwift"; - public static final boolean TRACE_DOWNCALLS = Boolean.getBoolean("jextract.trace.downcalls"); + public static final String SWIFT_RUNTIME_FUNCTIONS_DYLIB_NAME = "SwiftRuntimeFunctions"; private static final String STDLIB_MACOS_DYLIB_PATH = "/usr/lib/swift/libswiftCore.dylib"; @@ -46,10 +42,10 @@ public class SwiftKit { @SuppressWarnings("unused") private static final boolean INITIALIZED_LIBS = loadLibraries(false); - public static boolean loadLibraries(boolean loadSwiftKit) { + public static boolean loadLibraries(boolean loadSwiftRuntimeFunctions) { System.loadLibrary(STDLIB_DYLIB_NAME); - if (loadSwiftKit) { - System.loadLibrary(SWIFTKITSWIFT_DYLIB_NAME); + if (loadSwiftRuntimeFunctions) { + System.loadLibrary(SWIFT_RUNTIME_FUNCTIONS_DYLIB_NAME); } return true; } @@ -68,34 +64,7 @@ private static SymbolLookup getSymbolLookup() { } } - public SwiftKit() { - } - - public static void traceDowncall(Object... args) { - var ex = new RuntimeException(); - - String traceArgs = Arrays.stream(args) - .map(Object::toString) - .collect(Collectors.joining(", ")); - System.out.printf("[java][%s:%d] Downcall: %s.%s(%s)\n", - ex.getStackTrace()[1].getFileName(), - ex.getStackTrace()[1].getLineNumber(), - ex.getStackTrace()[1].getClassName(), - ex.getStackTrace()[1].getMethodName(), - traceArgs); - } - - public static void trace(Object... args) { - var ex = new RuntimeException(); - - String traceArgs = Arrays.stream(args) - .map(Object::toString) - .collect(Collectors.joining(", ")); - System.out.printf("[java][%s:%d] %s: %s\n", - ex.getStackTrace()[1].getFileName(), - ex.getStackTrace()[1].getLineNumber(), - ex.getStackTrace()[1].getMethodName(), - traceArgs); + public SwiftRuntime() { } static MemorySegment findOrThrow(String symbol) { @@ -103,50 +72,10 @@ static MemorySegment findOrThrow(String symbol) { .orElseThrow(() -> new UnsatisfiedLinkError("unresolved symbol: %s".formatted(symbol))); } - public static String getJavaLibraryPath() { - return System.getProperty("java.library.path"); - } - public static boolean getJextractTraceDowncalls() { return Boolean.getBoolean("jextract.trace.downcalls"); } - // ==== ------------------------------------------------------------------------------------------------------------ - // Loading libraries - - public static void loadLibrary(String libname) { - // TODO: avoid concurrent loadResource calls; one load is enough esp since we cause File IO when we do that - try { - // try to load a dylib from our classpath, e.g. when we included it in our jar - loadResourceLibrary(libname); - } catch (UnsatisfiedLinkError | RuntimeException e) { - // fallback to plain system path loading - System.loadLibrary(libname); - } - } - - public static void loadResourceLibrary(String libname) { - var resourceName = PlatformUtils.dynamicLibraryName(libname); - if (SwiftKit.TRACE_DOWNCALLS) { - System.out.println("[swift-java] Loading resource library: " + resourceName); - } - - try (var libInputStream = SwiftKit.class.getResourceAsStream("/" + resourceName)) { - if (libInputStream == null) { - throw new RuntimeException("Expected library '" + libname + "' ('" + resourceName + "') was not found as resource!"); - } - - // TODO: we could do an in memory file system here - var tempFile = File.createTempFile(libname, ""); - tempFile.deleteOnExit(); - Files.copy(libInputStream, tempFile.toPath(), StandardCopyOption.REPLACE_EXISTING); - - System.load(tempFile.getAbsolutePath()); - } catch (IOException e) { - throw new RuntimeException("Failed to load dynamic library '" + libname + "' ('" + resourceName + "') as resource!", e); - } - } - // ==== ------------------------------------------------------------------------------------------------------------ // free @@ -198,7 +127,7 @@ private static class swift_retainCount { public static long retainCount(MemorySegment object) { var mh$ = swift_retainCount.HANDLE; try { - if (TRACE_DOWNCALLS) { + if (CallTraces.TRACE_DOWNCALLS) { traceDowncall("swift_retainCount", object); } return (long) mh$.invokeExact(object); @@ -228,7 +157,7 @@ private static class swift_retain { public static void retain(MemorySegment object) { var mh$ = swift_retain.HANDLE; try { - if (TRACE_DOWNCALLS) { + if (CallTraces.TRACE_DOWNCALLS) { traceDowncall("swift_retain", object); } mh$.invokeExact(object); @@ -258,7 +187,7 @@ private static class swift_release { public static void release(MemorySegment object) { var mh$ = swift_release.HANDLE; try { - if (TRACE_DOWNCALLS) { + if (CallTraces.TRACE_DOWNCALLS) { traceDowncall("swift_release", object); } mh$.invokeExact(object); @@ -294,7 +223,7 @@ private static class swift_getTypeByName { public static MemorySegment getTypeByName(String string) { var mh$ = swift_getTypeByName.HANDLE; try { - if (TRACE_DOWNCALLS) { + if (CallTraces.TRACE_DOWNCALLS) { traceDowncall("_typeByName"); } // TODO: A bit annoying to generate, we need an arena for the conversion... @@ -349,7 +278,7 @@ public static Optional getTypeByMangledNameInEnvironment(String ma // contain this, but we don't need it for type lookup mangledName = stripSuffix(mangledName, "Ma"); mangledName = stripSuffix(mangledName, "CN"); - if (TRACE_DOWNCALLS) { + if (CallTraces.TRACE_DOWNCALLS) { traceDowncall("swift_getTypeByMangledNameInEnvironment", mangledName); } try (Arena arena = Arena.ofConfined()) { @@ -471,6 +400,42 @@ public static MemorySegment toCString(String str, Arena arena) { return arena.allocateFrom(str); } + public static MemorySegment toOptionalSegmentInt(OptionalInt opt, Arena arena) { + return opt.isPresent() ? arena.allocateFrom(ValueLayout.JAVA_INT, opt.getAsInt()) : MemorySegment.NULL; + } + + public static MemorySegment toOptionalSegmentLong(OptionalLong opt, Arena arena) { + return opt.isPresent() ? arena.allocateFrom(ValueLayout.JAVA_LONG, opt.getAsLong()) : MemorySegment.NULL; + } + + public static MemorySegment toOptionalSegmentDouble(OptionalDouble opt, Arena arena) { + return opt.isPresent() ? arena.allocateFrom(ValueLayout.JAVA_DOUBLE, opt.getAsDouble()) : MemorySegment.NULL; + } + + public static MemorySegment toOptionalSegmentBoolean(Optional opt, Arena arena) { + return opt.map(val -> arena.allocateFrom(ValueLayout.JAVA_BYTE, (byte) (val ? 1 : 0))).orElse(MemorySegment.NULL); + } + + public static MemorySegment toOptionalSegmentByte(Optional opt, Arena arena) { + return opt.map(val -> arena.allocateFrom(ValueLayout.JAVA_BYTE, val)).orElse(MemorySegment.NULL); + } + + public static MemorySegment toOptionalSegmentCharacter(Optional opt, Arena arena) { + return opt.map(val -> arena.allocateFrom(ValueLayout.JAVA_CHAR, val)).orElse(MemorySegment.NULL); + } + + public static MemorySegment toOptionalSegmentShort(Optional opt, Arena arena) { + return opt.map(val -> arena.allocateFrom(ValueLayout.JAVA_SHORT, val)).orElse(MemorySegment.NULL); + } + + public static MemorySegment toOptionalSegmentFloat(Optional opt, Arena arena) { + return opt.map(val -> arena.allocateFrom(ValueLayout.JAVA_FLOAT, val)).orElse(MemorySegment.NULL); + } + + public static MemorySegment toOptionalSegmentInstance(Optional opt) { + return opt.map(instance -> instance.$memorySegment()).orElse(MemorySegment.NULL); + } + private static class swift_getTypeName { /** @@ -496,4 +461,30 @@ private static class swift_getTypeName { public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } + public static void log(SwiftJavaLogGroup group, String message) { + if (group.isEnabled()) { + System.err.println(message); + } + } + + public static void log(SwiftJavaLogGroup group, String format, String ...args) { + if (group.isEnabled()) { + System.err.println(String.format(format, (Object[]) args)); + } + } + +} + +enum SwiftJavaLogGroup { + LIFECYCLE; + + static boolean LOG_LIFECYCLE = + Boolean.getBoolean("swift-java.log.lifecycle"); + + boolean isEnabled() { + switch (this) { + case LIFECYCLE: return LOG_LIFECYCLE; + } + throw new IllegalArgumentException("Not handled log group: " + this); + } } diff --git a/SwiftKit/src/main/java/org/swift/swiftkit/SwiftValueLayout.java b/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/SwiftValueLayout.java similarity index 84% rename from SwiftKit/src/main/java/org/swift/swiftkit/SwiftValueLayout.java rename to SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/SwiftValueLayout.java index dea52154f..be883e051 100644 --- a/SwiftKit/src/main/java/org/swift/swiftkit/SwiftValueLayout.java +++ b/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/SwiftValueLayout.java @@ -12,7 +12,7 @@ // //===----------------------------------------------------------------------===// -package org.swift.swiftkit; +package org.swift.swiftkit.ffm; import java.lang.foreign.AddressLayout; import java.lang.foreign.MemoryLayout; @@ -34,15 +34,25 @@ public static long addressByteSize() { } public static final ValueLayout.OfBoolean SWIFT_BOOL = ValueLayout.JAVA_BOOLEAN; + public static final ValueLayout.OfByte SWIFT_INT8 = ValueLayout.JAVA_BYTE; - public static final ValueLayout.OfChar SWIFT_UINT16 = ValueLayout.JAVA_CHAR; + public static final ValueLayout.OfByte SWIFT_UINT8 = SWIFT_INT8; + public static final ValueLayout.OfShort SWIFT_INT16 = ValueLayout.JAVA_SHORT; + public static final ValueLayout.OfChar SWIFT_UINT16 = ValueLayout.JAVA_CHAR; + public static final ValueLayout.OfInt SWIFT_INT32 = ValueLayout.JAVA_INT; + public static final ValueLayout.OfInt SWIFT_UINT32 = SWIFT_INT32; + public static final ValueLayout.OfLong SWIFT_INT64 = ValueLayout.JAVA_LONG; + public static final ValueLayout.OfLong SWIFT_UINT64 = SWIFT_INT64; + public static final ValueLayout.OfFloat SWIFT_FLOAT = ValueLayout.JAVA_FLOAT; public static final ValueLayout.OfDouble SWIFT_DOUBLE = ValueLayout.JAVA_DOUBLE; + + // FIXME: this sequence layout is a workaround, we must properly size pointers when we get them. public static final AddressLayout SWIFT_POINTER = ValueLayout.ADDRESS - .withTargetLayout(MemoryLayout.sequenceLayout(Long.MAX_VALUE, JAVA_BYTE)); + .withTargetLayout(MemoryLayout.sequenceLayout(Long.MAX_VALUE, JAVA_BYTE)); public static final SequenceLayout SWIFT_BYTE_ARRAY = MemoryLayout.sequenceLayout(8, ValueLayout.JAVA_BYTE); /** diff --git a/SwiftKit/src/main/java/org/swift/swiftkit/SwiftValueWitnessTable.java b/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/SwiftValueWitnessTable.java similarity index 88% rename from SwiftKit/src/main/java/org/swift/swiftkit/SwiftValueWitnessTable.java rename to SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/SwiftValueWitnessTable.java index 53680f5fa..2b918523d 100644 --- a/SwiftKit/src/main/java/org/swift/swiftkit/SwiftValueWitnessTable.java +++ b/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/SwiftValueWitnessTable.java @@ -12,13 +12,12 @@ // //===----------------------------------------------------------------------===// -package org.swift.swiftkit; +package org.swift.swiftkit.ffm; import java.lang.foreign.*; import java.lang.invoke.*; import static java.lang.foreign.ValueLayout.JAVA_BYTE; -import static org.swift.swiftkit.SwiftKit.getSwiftInt; public abstract class SwiftValueWitnessTable { @@ -56,7 +55,7 @@ public abstract class SwiftValueWitnessTable { MemoryLayout.PathElement.groupElement("vwt")); /** - * Given the address of Swift type metadata for a type, return the addres + * Given the address of Swift type metadata for a type, return the address * of the "full" type metadata that can be accessed via fullTypeMetadataLayout. */ public static MemorySegment fullTypeMetadata(MemorySegment typeMetadata) { @@ -71,7 +70,6 @@ public static MemorySegment fullTypeMetadata(MemorySegment typeMetadata) { public static MemorySegment valueWitnessTable(MemorySegment typeMetadata) { return fullTypeMetadata(typeMetadata) .get(SwiftValueLayout.SWIFT_POINTER, SwiftValueWitnessTable.fullTypeMetadata$vwt$offset); -// .get(ValueLayout.ADDRESS, SwiftValueWitnessTable.fullTypeMetadata$vwt$offset); } @@ -87,7 +85,7 @@ public static MemorySegment valueWitnessTable(MemorySegment typeMetadata) { * @param typeMetadata the memory segment must point to a Swift metadata */ public static long sizeOfSwiftType(MemorySegment typeMetadata) { - return getSwiftInt(valueWitnessTable(typeMetadata), SwiftValueWitnessTable.$size$offset); + return SwiftRuntime.getSwiftInt(valueWitnessTable(typeMetadata), SwiftValueWitnessTable.$size$offset); } @@ -113,7 +111,7 @@ public static long sizeOfSwiftType(MemorySegment typeMetadata) { * @param typeMetadata the memory segment must point to a Swift metadata */ public static long strideOfSwiftType(MemorySegment typeMetadata) { - return getSwiftInt(valueWitnessTable(typeMetadata), SwiftValueWitnessTable.$stride$offset); + return SwiftRuntime.getSwiftInt(valueWitnessTable(typeMetadata), SwiftValueWitnessTable.$stride$offset); } @@ -123,7 +121,7 @@ public static long strideOfSwiftType(MemorySegment typeMetadata) { * @param typeMetadata the memory segment must point to a Swift metadata */ public static long alignmentOfSwiftType(MemorySegment typeMetadata) { - long flags = getSwiftInt(valueWitnessTable(typeMetadata), SwiftValueWitnessTable.$flags$offset); + long flags = SwiftRuntime.getSwiftInt(valueWitnessTable(typeMetadata), SwiftValueWitnessTable.$flags$offset); return (flags & 0xFF) + 1; } @@ -157,7 +155,7 @@ public static MemoryLayout layoutOfSwiftType(MemorySegment typeMetadata) { return MemoryLayout.structLayout( layouts - ).withName(SwiftKit.nameOfSwiftType(typeMetadata, true)); + ).withName(SwiftRuntime.nameOfSwiftType(typeMetadata, true)); } @@ -187,8 +185,8 @@ private static class destroy { $LAYOUT.byteOffset(MemoryLayout.PathElement.groupElement("destroy")); static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( - ValueLayout.ADDRESS, // witness table functions expect a pointer to self pointer - ValueLayout.ADDRESS // pointer to the witness table + ValueLayout.ADDRESS, // pointer to self + ValueLayout.ADDRESS // pointer to the type metadata ); /** @@ -199,7 +197,7 @@ static MemorySegment addr(SwiftAnyType ty) { final var vwt = SwiftValueWitnessTable.valueWitnessTable(ty.$memorySegment()); // Get the address of the destroy function stored at the offset of the witness table - long funcAddress = getSwiftInt(vwt, destroy.$offset); + long funcAddress = SwiftRuntime.getSwiftInt(vwt, destroy.$offset); return MemorySegment.ofAddress(funcAddress); } @@ -215,13 +213,9 @@ static MethodHandle handle(SwiftAnyType ty) { * This includes deallocating the Swift managed memory for the object. */ public static void destroy(SwiftAnyType type, MemorySegment object) { - var fullTypeMetadata = fullTypeMetadata(type.$memorySegment()); - var wtable = valueWitnessTable(fullTypeMetadata); - var mh = destroy.handle(type); - try { - mh.invokeExact(object, wtable); + mh.invokeExact(object, type.$memorySegment()); } catch (Throwable th) { throw new AssertionError("Failed to destroy '" + type + "' at " + object, th); } @@ -248,7 +242,7 @@ private static class initializeWithCopy { /* -> */ ValueLayout.ADDRESS, // returns the destination object ValueLayout.ADDRESS, // destination ValueLayout.ADDRESS, // source - ValueLayout.ADDRESS // pointer to the witness table + ValueLayout.ADDRESS // pointer to the type metadata ); /** @@ -259,7 +253,7 @@ static MemorySegment addr(SwiftAnyType ty) { final var vwt = SwiftValueWitnessTable.valueWitnessTable(ty.$memorySegment()); // Get the address of the function stored at the offset of the witness table - long funcAddress = getSwiftInt(vwt, initializeWithCopy.$offset); + long funcAddress = SwiftRuntime.getSwiftInt(vwt, initializeWithCopy.$offset); return MemorySegment.ofAddress(funcAddress); } @@ -276,13 +270,10 @@ static MethodHandle handle(SwiftAnyType ty) { * Returns the dest object. */ public static MemorySegment initializeWithCopy(SwiftAnyType type, MemorySegment dest, MemorySegment src) { - var fullTypeMetadata = fullTypeMetadata(type.$memorySegment()); - var wtable = valueWitnessTable(fullTypeMetadata); - var mh = initializeWithCopy.handle(type); try { - return (MemorySegment) mh.invokeExact(dest, src, wtable); + return (MemorySegment) mh.invokeExact(dest, src, type.$memorySegment()); } catch (Throwable th) { throw new AssertionError("Failed to initializeWithCopy '" + type + "' (" + dest + ", " + src + ")", th); } diff --git a/SwiftKit/src/test/java/org/swift/swiftkit/AutoArenaTest.java b/SwiftKitFFM/src/test/java/org/swift/swiftkit/ffm/AutoArenaTest.java similarity index 81% rename from SwiftKit/src/test/java/org/swift/swiftkit/AutoArenaTest.java rename to SwiftKitFFM/src/test/java/org/swift/swiftkit/ffm/AutoArenaTest.java index 23365de91..18c0a5af0 100644 --- a/SwiftKit/src/test/java/org/swift/swiftkit/AutoArenaTest.java +++ b/SwiftKitFFM/src/test/java/org/swift/swiftkit/ffm/AutoArenaTest.java @@ -12,13 +12,12 @@ // //===----------------------------------------------------------------------===// -package org.swift.swiftkit; +package org.swift.swiftkit.ffm; import org.junit.jupiter.api.Test; import java.lang.foreign.GroupLayout; import java.lang.foreign.MemorySegment; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; public class AutoArenaTest { @@ -26,7 +25,7 @@ public class AutoArenaTest { @Test @SuppressWarnings("removal") // System.runFinalization() will be removed public void cleaner_releases_native_resource() { - SwiftArena arena = SwiftArena.ofAuto(); + AllocatingSwiftArena arena = AllocatingSwiftArena.ofAuto(); // This object is registered to the arena. var object = new FakeSwiftInstance(arena); @@ -48,16 +47,11 @@ public void cleaner_releases_native_resource() { } } - private static class FakeSwiftInstance extends SwiftInstance implements SwiftHeapObject { - public FakeSwiftInstance(SwiftArena arena) { + private static class FakeSwiftInstance extends FFMSwiftInstance implements SwiftHeapObject { + public FakeSwiftInstance(AllocatingSwiftArena arena) { super(MemorySegment.NULL, arena); } - @Override - public GroupLayout $layout() { - return null; - } - @Override public SwiftAnyType $swiftType() { return null; diff --git a/SwiftKitFFM/src/test/java/org/swift/swiftkit/ffm/SwiftRuntimeMetadataTest.java b/SwiftKitFFM/src/test/java/org/swift/swiftkit/ffm/SwiftRuntimeMetadataTest.java new file mode 100644 index 000000000..0ec821238 --- /dev/null +++ b/SwiftKitFFM/src/test/java/org/swift/swiftkit/ffm/SwiftRuntimeMetadataTest.java @@ -0,0 +1,57 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package org.swift.swiftkit.ffm; + +public class SwiftRuntimeMetadataTest { + +// @Test +// public void integer_layout_metadata() { +// SwiftAnyType swiftType = SwiftKit.getTypeByMangledNameInEnvironment("Si").get(); +// +// if (SwiftValueLayout.addressByteSize() == 4) { +// // 32-bit platform +// Assertions.assertEquals(8, SwiftValueWitnessTable.sizeOfSwiftType(swiftType.$memorySegment())); +// Assertions.assertEquals(8, SwiftValueWitnessTable.strideOfSwiftType(swiftType.$memorySegment())); +// Assertions.assertEquals(8, SwiftValueWitnessTable.alignmentOfSwiftType(swiftType.$memorySegment())); +// Assertions.assertEquals("[8%[9:b1]x7](Swift.Int)", SwiftValueWitnessTable.layoutOfSwiftType(swiftType.$memorySegment()).toString()); +// } else { +// // 64-bit platform +// Assertions.assertEquals(8, SwiftValueWitnessTable.sizeOfSwiftType(swiftType.$memorySegment())); +// Assertions.assertEquals(8, SwiftValueWitnessTable.strideOfSwiftType(swiftType.$memorySegment())); +// Assertions.assertEquals(8, SwiftValueWitnessTable.alignmentOfSwiftType(swiftType.$memorySegment())); +// Assertions.assertEquals("[8%[8:b1]](Swift.Int)", SwiftValueWitnessTable.layoutOfSwiftType(swiftType.$memorySegment()).toString()); +// } +// } +// +// @Test +// public void optional_integer_layout_metadata() { +// SwiftAnyType swiftType = SwiftKit.getTypeByMangledNameInEnvironment("SiSg").get(); +// +// if (SwiftValueLayout.addressByteSize() == 4) { +// // 64-bit platform +// Assertions.assertEquals(9, SwiftValueWitnessTable.sizeOfSwiftType(swiftType.$memorySegment())); +// Assertions.assertEquals(16, SwiftValueWitnessTable.strideOfSwiftType(swiftType.$memorySegment())); +// Assertions.assertEquals(8, SwiftValueWitnessTable.alignmentOfSwiftType(swiftType.$memorySegment())); +// Assertions.assertEquals("[8%[9:b1]x7](Swift.Optional)", SwiftValueWitnessTable.layoutOfSwiftType(swiftType.$memorySegment()).toString()); +// } else { +// // 64-bit platform +// Assertions.assertEquals(9, SwiftValueWitnessTable.sizeOfSwiftType(swiftType.$memorySegment())); +// Assertions.assertEquals(16, SwiftValueWitnessTable.strideOfSwiftType(swiftType.$memorySegment())); +// Assertions.assertEquals(8, SwiftValueWitnessTable.alignmentOfSwiftType(swiftType.$memorySegment())); +// Assertions.assertEquals("[8%[9:b1]x7](Swift.Optional)", SwiftValueWitnessTable.layoutOfSwiftType(swiftType.$memorySegment()).toString()); +// } +// } + +} diff --git a/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift b/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift index 5dd28cc8c..06d1d5553 100644 --- a/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift +++ b/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// @_spi(Testing) import JExtractSwiftLib +import SwiftJavaConfigurationShared import SwiftSyntax import Testing @@ -31,9 +32,9 @@ func assertLoweredFunction( line: Int = #line, column: Int = #column ) throws { - let translator = Swift2JavaTranslator( - swiftModuleName: swiftModuleName - ) + var config = Configuration() + config.swiftModule = swiftModuleName + let translator = Swift2JavaTranslator(config: config) if let sourceFile { translator.add(filePath: "Fake.swift", text: sourceFile) @@ -42,6 +43,7 @@ func assertLoweredFunction( translator.prepareForTranslation() let generator = FFMSwift2JavaGenerator( + config: config, translator: translator, javaPackage: "com.example.swift", swiftOutputDirectory: "/fake", @@ -72,8 +74,7 @@ func assertLoweredFunction( let loweredCDecl = loweredFunction.cdeclThunk( cName: "c_\(swiftFunctionName)", swiftAPIName: swiftFunctionName, - as: apiKind, - stdlibTypes: translator.swiftStdlibTypes + as: apiKind ) #expect( @@ -117,9 +118,9 @@ func assertLoweredVariableAccessor( line: Int = #line, column: Int = #column ) throws { - let translator = Swift2JavaTranslator( - swiftModuleName: swiftModuleName - ) + var config = Configuration() + config.swiftModule = swiftModuleName + let translator = Swift2JavaTranslator(config: config) if let sourceFile { translator.add(filePath: "Fake.swift", text: sourceFile) @@ -128,6 +129,7 @@ func assertLoweredVariableAccessor( translator.prepareForTranslation() let generator = FFMSwift2JavaGenerator( + config: config, translator: translator, javaPackage: javaPackage, swiftOutputDirectory: "/fake", @@ -140,8 +142,7 @@ func assertLoweredVariableAccessor( let loweredCDecl = loweredFunction?.cdeclThunk( cName: "c_\(swiftVariableName)", swiftAPIName: swiftVariableName, - as: isSet ? .setter : .getter, - stdlibTypes: translator.swiftStdlibTypes + as: isSet ? .setter : .getter ) #expect( diff --git a/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift b/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift index 3ea03d981..67671548b 100644 --- a/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift +++ b/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift @@ -14,6 +14,7 @@ import JExtractSwiftLib import Testing +import SwiftJavaConfigurationShared import struct Foundation.CharacterSet enum RenderKind { @@ -23,51 +24,83 @@ enum RenderKind { func assertOutput( dump: Bool = false, - _ translator: Swift2JavaTranslator, input: String, + config: Configuration? = nil, + _ mode: JExtractGenerationMode, _ renderKind: RenderKind, - detectChunkByInitialLines: Int = 4, + swiftModuleName: String = "SwiftModule", + detectChunkByInitialLines _detectChunkByInitialLines: Int = 4, + javaClassLookupTable: [String: String] = [:], expectedChunks: [String], fileID: String = #fileID, filePath: String = #filePath, line: Int = #line, column: Int = #column ) throws { - try! translator.analyze(file: "/fake/Fake.swiftinterface", text: input) + var config = config ?? Configuration() + config.swiftModule = swiftModuleName + let translator = Swift2JavaTranslator(config: config) + translator.dependenciesClasses = Array(javaClassLookupTable.keys) - let generator = FFMSwift2JavaGenerator( - translator: translator, - javaPackage: "com.example.swift", - swiftOutputDirectory: "/fake", - javaOutputDirectory: "/fake" - ) + try! translator.analyze(path: "/fake/Fake.swiftinterface", text: input) let output: String var printer: CodePrinter = CodePrinter(mode: .accumulateAll) - switch renderKind { - case .swift: - try generator.writeSwiftThunkSources(printer: &printer) - case .java: - try generator.writeExportedJavaSources(printer: &printer) + switch mode { + case .ffm: + let generator = FFMSwift2JavaGenerator( + config: config, + translator: translator, + javaPackage: "com.example.swift", + swiftOutputDirectory: "/fake", + javaOutputDirectory: "/fake" + ) + + switch renderKind { + case .swift: + try generator.writeSwiftThunkSources(printer: &printer) + case .java: + try generator.writeExportedJavaSources(printer: &printer) + } + + case .jni: + let generator = JNISwift2JavaGenerator( + config: config, + translator: translator, + javaPackage: "com.example.swift", + swiftOutputDirectory: "/fake", + javaOutputDirectory: "/fake", + javaClassLookupTable: javaClassLookupTable + ) + + switch renderKind { + case .swift: + try generator.writeSwiftThunkSources(&printer) + case .java: + try generator.writeExportedJavaSources(&printer) + } } output = printer.finalize() let gotLines = output.split(separator: "\n").filter { l in l.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines).count > 0 } - for expected in expectedChunks { - let expectedLines = expected.split(separator: "\n") + for expectedChunk in expectedChunks { + let expectedLines = expectedChunk.split(separator: "\n") + let detectChunkByInitialLines = min(expectedLines.count, _detectChunkByInitialLines) + precondition(detectChunkByInitialLines > 0, "Chunk size to detect cannot be zero lines!") var matchingOutputOffset: Int? = nil let expectedInitialMatchingLines = expectedLines[0.. (offset+detectChunkByInitialLines) { - let textLinesAtOffset = gotLines[offset.. (lineOffset+detectChunkByInitialLines) { + let textLinesAtOffset = gotLines[lineOffset..= printFromLineNo && n <= printToLineNo { - print("\(n): \(g)".red(if: diffLineNumbers.contains(n))) + for (n, g) in gotLines.enumerated() where n >= printFromLineNo { + let baseLine = "\(n): \(g)" + print(baseLine) } print("==== ---------------------------------------------------------------\n") } @@ -176,14 +242,18 @@ func assertOutput( print("==== ---------------------------------------------------------------") print("Expected output:") for (n, e) in expectedLines.enumerated() { - print("\(e)".yellow(if: diffLineNumbers.contains(n))) + let isMismatch = diffLineNumbers.contains(n) + let marker = isMismatch ? " // <<<<<<<< error: mismatch" : "" + print("\(e)\(marker)".yellow(if: isMismatch)) } } print("==== ---------------------------------------------------------------") print("Got output:") for (n, g) in gotLines.enumerated() { - print("\(g)".red(if: diffLineNumbers.contains(n))) + let isMismatch = diffLineNumbers.contains(n) + let marker = isMismatch ? "// <<<<<<<< error: mismatch" : "" + print("\(g)\(marker)".red(if: isMismatch)) } print("==== ---------------------------------------------------------------\n") } diff --git a/Tests/JExtractSwiftTests/ClassPrintingTests.swift b/Tests/JExtractSwiftTests/ClassPrintingTests.swift index f0c594562..12b11d387 100644 --- a/Tests/JExtractSwiftTests/ClassPrintingTests.swift +++ b/Tests/JExtractSwiftTests/ClassPrintingTests.swift @@ -41,14 +41,10 @@ struct ClassPrintingTests { @Test("Import: class layout") func class_layout() throws { - let st = Swift2JavaTranslator( - swiftModuleName: "__FakeModule" - ) - - try assertOutput(st, input: class_interfaceFile, .java, expectedChunks: [ + try assertOutput(input: class_interfaceFile, .ffm, .java, swiftModuleName: "__FakeModule", expectedChunks: [ """ public static final SwiftAnyType TYPE_METADATA = - new SwiftAnyType(SwiftKit.swiftjava.getType("__FakeModule", "MySwiftClass")); + new SwiftAnyType(SwiftRuntime.swiftjava.getType("__FakeModule", "MySwiftClass")); public final SwiftAnyType $swiftType() { return TYPE_METADATA; } diff --git a/Tests/JExtractSwiftTests/DataImportTests.swift b/Tests/JExtractSwiftTests/DataImportTests.swift new file mode 100644 index 000000000..bad4174db --- /dev/null +++ b/Tests/JExtractSwiftTests/DataImportTests.swift @@ -0,0 +1,453 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JExtractSwiftLib +import Testing + +final class DataImportTests { + private static let ifConfigImport = """ + #if canImport(FoundationEssentials) + import FoundationEssentials + #else + import Foundation + #endif + """ + private static let foundationData_interfaceFile = + """ + import Foundation + + public func receiveData(dat: Data) + public func returnData() -> Data + """ + + private static let foundationDataProtocol_interfaceFile = + """ + import Foundation + + public func receiveDataProtocol(dat: some DataProtocol, dat2: T?) + """ + + private static let essentialsData_interfaceFile = + """ + import FoundationEssentials + + public func receiveData(dat: Data) + public func returnData() -> Data + """ + + private static let essentialsDataProtocol_interfaceFile = + """ + import FoundationEssentials + + public func receiveDataProtocol(dat: some DataProtocol, dat2: T?) + """ + private static let ifConfigData_interfaceFile = + """ + \(ifConfigImport) + + public func receiveData(dat: Data) + public func returnData() -> Data + """ + + private static let ifConfigDataProtocol_interfaceFile = + """ + \(ifConfigImport) + + public func receiveDataProtocol(dat: some DataProtocol, dat2: T?) + """ + + @Test("Import Data: Swift thunks", arguments: zip( + [Self.foundationData_interfaceFile, Self.essentialsData_interfaceFile, Self.ifConfigData_interfaceFile], + ["import Foundation", "import FoundationEssentials", Self.ifConfigImport] + )) + func data_swiftThunk(fileContent: String, expectedImportChunk: String) throws { + + try assertOutput( + input: fileContent, .ffm, .swift, + detectChunkByInitialLines: 10, + expectedChunks: [ + expectedImportChunk, + """ + @_cdecl("swiftjava_SwiftModule_receiveData_dat") + public func swiftjava_SwiftModule_receiveData_dat(_ dat: UnsafeRawPointer) { + receiveData(dat: dat.assumingMemoryBound(to: Data.self).pointee) + } + """, + """ + @_cdecl("swiftjava_SwiftModule_returnData") + public func swiftjava_SwiftModule_returnData(_ _result: UnsafeMutableRawPointer) { + _result.assumingMemoryBound(to: Data.self).initialize(to: returnData()) + } + """, + + """ + @_cdecl("swiftjava_getType_SwiftModule_Data") + public func swiftjava_getType_SwiftModule_Data() -> UnsafeMutableRawPointer /* Any.Type */ { + return unsafeBitCast(Data.self, to: UnsafeMutableRawPointer.self) + } + """, + + """ + @_cdecl("swiftjava_SwiftModule_Data_init_bytes_count") + public func swiftjava_SwiftModule_Data_init_bytes_count(_ bytes: UnsafeRawPointer, _ count: Int, _ _result: UnsafeMutableRawPointer) { + _result.assumingMemoryBound(to: Data.self).initialize(to: Data(bytes: bytes, count: count)) + } + """, + + """ + @_cdecl("swiftjava_SwiftModule_Data_count$get") + public func swiftjava_SwiftModule_Data_count$get(_ self: UnsafeRawPointer) -> Int { + return self.assumingMemoryBound(to: Data.self).pointee.count + } + """, + + """ + @_cdecl("swiftjava_SwiftModule_Data_withUnsafeBytes__") + public func swiftjava_SwiftModule_Data_withUnsafeBytes__(_ body: @convention(c) (UnsafeRawPointer?, Int) -> Void, _ self: UnsafeRawPointer) { + self.assumingMemoryBound(to: Data.self).pointee.withUnsafeBytes({ (_0) in + return body(_0.baseAddress, _0.count) + }) + } + """, + ] + ) + } + + @Test("Import Data: JavaBindings", arguments: [ + Self.foundationData_interfaceFile, Self.essentialsData_interfaceFile, Self.ifConfigData_interfaceFile + ]) + func data_javaBindings(fileContent: String) throws { + try assertOutput( + input: fileContent, .ffm, .java, + expectedChunks: [ + """ + /** + * {@snippet lang=c : + * void swiftjava_SwiftModule_receiveData_dat(const void *dat) + * } + */ + private static class swiftjava_SwiftModule_receiveData_dat { + private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + /* dat: */SwiftValueLayout.SWIFT_POINTER + ); + private static final MemorySegment ADDR = + SwiftModule.findOrThrow("swiftjava_SwiftModule_receiveData_dat"); + private static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + public static void call(java.lang.foreign.MemorySegment dat) { + try { + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall(dat); + } + HANDLE.invokeExact(dat); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + } + """, + + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func receiveData(dat: Data) + * } + */ + public static void receiveData(Data dat) { + swiftjava_SwiftModule_receiveData_dat.call(dat.$memorySegment()); + } + """, + + """ + /** + * {@snippet lang=c : + * void swiftjava_SwiftModule_returnData(void *_result) + * } + */ + private static class swiftjava_SwiftModule_returnData { + private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + /* _result: */SwiftValueLayout.SWIFT_POINTER + ); + private static final MemorySegment ADDR = + SwiftModule.findOrThrow("swiftjava_SwiftModule_returnData"); + private static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + public static void call(java.lang.foreign.MemorySegment _result) { + try { + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall(_result); + } + HANDLE.invokeExact(_result); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + } + """, + + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func returnData() -> Data + * } + */ + public static Data returnData(AllocatingSwiftArena swiftArena$) { + MemorySegment _result = swiftArena$.allocate(Data.$LAYOUT); + swiftjava_SwiftModule_returnData.call(_result); + return Data.wrapMemoryAddressUnsafe(_result, swiftArena$); + } + """, + + + """ + /** + * {@snippet lang=c : + * void swiftjava_SwiftModule_Data_init_bytes_count(const void *bytes, ptrdiff_t count, void *_result) + * } + */ + private static class swiftjava_SwiftModule_Data_init_bytes_count { + private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + /* bytes: */SwiftValueLayout.SWIFT_POINTER, + /* count: */SwiftValueLayout.SWIFT_INT, + /* _result: */SwiftValueLayout.SWIFT_POINTER + ); + private static final MemorySegment ADDR = + SwiftModule.findOrThrow("swiftjava_SwiftModule_Data_init_bytes_count"); + private static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + public static void call(java.lang.foreign.MemorySegment bytes, long count, java.lang.foreign.MemorySegment _result) { + try { + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall(bytes, count, _result); + } + HANDLE.invokeExact(bytes, count, _result); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + } + """, + + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public init(bytes: UnsafeRawPointer, count: Int) + * } + */ + public static Data init(java.lang.foreign.MemorySegment bytes, long count, AllocatingSwiftArena swiftArena$) { + MemorySegment _result = swiftArena$.allocate(Data.$LAYOUT); + swiftjava_SwiftModule_Data_init_bytes_count.call(bytes, count, _result); + return Data.wrapMemoryAddressUnsafe(_result, swiftArena$); + } + """, + + """ + /** + * {@snippet lang=c : + * ptrdiff_t swiftjava_SwiftModule_Data_count$get(const void *self) + * } + */ + private static class swiftjava_SwiftModule_Data_count$get { + private static final FunctionDescriptor DESC = FunctionDescriptor.of( + /* -> */SwiftValueLayout.SWIFT_INT, + /* self: */SwiftValueLayout.SWIFT_POINTER + ); + private static final MemorySegment ADDR = + SwiftModule.findOrThrow("swiftjava_SwiftModule_Data_count$get"); + private static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + public static long call(java.lang.foreign.MemorySegment self) { + try { + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall(self); + } + return (long) HANDLE.invokeExact(self); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + } + """, + + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public var count: Int + * } + */ + public long getCount() { + $ensureAlive(); + return swiftjava_SwiftModule_Data_count$get.call(this.$memorySegment()); + } + """, + + """ + /** + * {@snippet lang=c : + * void swiftjava_SwiftModule_Data_withUnsafeBytes__(void (*body)(const void *, ptrdiff_t), const void *self) + * } + */ + private static class swiftjava_SwiftModule_Data_withUnsafeBytes__ { + private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + /* body: */SwiftValueLayout.SWIFT_POINTER, + /* self: */SwiftValueLayout.SWIFT_POINTER + ); + private static final MemorySegment ADDR = + SwiftModule.findOrThrow("swiftjava_SwiftModule_Data_withUnsafeBytes__"); + private static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + public static void call(java.lang.foreign.MemorySegment body, java.lang.foreign.MemorySegment self) { + try { + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall(body, self); + } + HANDLE.invokeExact(body, self); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + /** + * {snippet lang=c : + * void (*)(const void *, ptrdiff_t) + * } + */ + private static class $body { + @FunctionalInterface + public interface Function { + void apply(java.lang.foreign.MemorySegment _0, long _1); + } + private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + /* _0: */SwiftValueLayout.SWIFT_POINTER, + /* _1: */SwiftValueLayout.SWIFT_INT + ); + private static final MethodHandle HANDLE = SwiftRuntime.upcallHandle(Function.class, "apply", DESC); + private static MemorySegment toUpcallStub(Function fi, Arena arena) { + return Linker.nativeLinker().upcallStub(HANDLE.bindTo(fi), DESC, arena); + } + } + } + """, + + """ + public static class withUnsafeBytes { + @FunctionalInterface + public interface body { + void apply(java.lang.foreign.MemorySegment _0); + } + private static MemorySegment $toUpcallStub(body fi, Arena arena) { + return swiftjava_SwiftModule_Data_withUnsafeBytes__.$body.toUpcallStub((_0_pointer, _0_count) -> { + fi.apply(_0_pointer.reinterpret(_0_count)); + }, arena); + } + } + """, + + + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) -> Void) + * } + */ + public void withUnsafeBytes(withUnsafeBytes.body body) { + $ensureAlive(); + try(var arena$ = Arena.ofConfined()) { + swiftjava_SwiftModule_Data_withUnsafeBytes__.call(withUnsafeBytes.$toUpcallStub(body, arena$), this.$memorySegment()); + } + } + """ + ] + ) + } + + @Test("Import DataProtocol: Swift thunks", arguments: zip( + [Self.foundationDataProtocol_interfaceFile, Self.essentialsDataProtocol_interfaceFile, Self.ifConfigDataProtocol_interfaceFile], + ["import Foundation", "import FoundationEssentials", Self.ifConfigImport] + )) + func dataProtocol_swiftThunk(fileContent: String, expectedImportChunk: String) throws { + try assertOutput( + input: fileContent, .ffm, .swift, + expectedChunks: [ + expectedImportChunk, + """ + @_cdecl("swiftjava_SwiftModule_receiveDataProtocol_dat_dat2") + public func swiftjava_SwiftModule_receiveDataProtocol_dat_dat2(_ dat: UnsafeRawPointer, _ dat2: UnsafeRawPointer?) { + receiveDataProtocol(dat: dat.assumingMemoryBound(to: Data.self).pointee, dat2: dat2?.assumingMemoryBound(to: Data.self).pointee) + } + """, + + // Just to make sure 'Data' is imported. + """ + @_cdecl("swiftjava_getType_SwiftModule_Data") + """ + ] + ) + } + + @Test("Import DataProtocol: JavaBindings", arguments: [ + Self.foundationDataProtocol_interfaceFile, Self.essentialsDataProtocol_interfaceFile, Self.ifConfigDataProtocol_interfaceFile + ]) + func dataProtocol_javaBindings(fileContent: String) throws { + + try assertOutput( + input: fileContent, .ffm, .java, + expectedChunks: [ + """ + /** + * {@snippet lang=c : + * void swiftjava_SwiftModule_receiveDataProtocol_dat_dat2(const void *dat, const void *dat2) + * } + */ + private static class swiftjava_SwiftModule_receiveDataProtocol_dat_dat2 { + private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + /* dat: */SwiftValueLayout.SWIFT_POINTER, + /* dat2: */SwiftValueLayout.SWIFT_POINTER + ); + private static final MemorySegment ADDR = + SwiftModule.findOrThrow("swiftjava_SwiftModule_receiveDataProtocol_dat_dat2"); + private static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + public static void call(java.lang.foreign.MemorySegment dat, java.lang.foreign.MemorySegment dat2) { + try { + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall(dat, dat2); + } + HANDLE.invokeExact(dat, dat2); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + } + """, + + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func receiveDataProtocol(dat: some DataProtocol, dat2: T?) + * } + */ + public static void receiveDataProtocol(Data dat, Optional dat2) { + swiftjava_SwiftModule_receiveDataProtocol_dat_dat2.call(dat.$memorySegment(), SwiftRuntime.toOptionalSegmentInstance(dat2)); + } + """, + + // Just to make sure 'Data' is imported. + """ + public final class Data extends FFMSwiftInstance implements SwiftValue { + """ + ] + ) + } + +} diff --git a/Tests/JExtractSwiftTests/ExtensionImportTests.swift b/Tests/JExtractSwiftTests/ExtensionImportTests.swift new file mode 100644 index 000000000..2bad2c7b8 --- /dev/null +++ b/Tests/JExtractSwiftTests/ExtensionImportTests.swift @@ -0,0 +1,48 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JExtractSwiftLib +import Testing + +final class ExtensionImportTests { + let interfaceFile = + """ + extension MyStruct { + public func methodInExtension() {} + } + + public struct MyStruct {} + """ + + @Test("Import extensions: Swift thunks") + func data_swiftThunk() throws { + try assertOutput( + input: interfaceFile, .ffm, .swift, + expectedChunks: [ + """ + @_cdecl("swiftjava_getType_SwiftModule_MyStruct") + public func swiftjava_getType_SwiftModule_MyStruct() -> UnsafeMutableRawPointer /* Any.Type */ { + return unsafeBitCast(MyStruct.self, to: UnsafeMutableRawPointer.self) + } + """, + """ + @_cdecl("swiftjava_SwiftModule_MyStruct_methodInExtension") + public func swiftjava_SwiftModule_MyStruct_methodInExtension(_ self: UnsafeRawPointer) { + self.assumingMemoryBound(to: MyStruct.self).pointee.methodInExtension() + } + """ + ] + ) + } +} diff --git a/Tests/JExtractSwiftTests/FFM/FFMSubscriptsTests.swift b/Tests/JExtractSwiftTests/FFM/FFMSubscriptsTests.swift new file mode 100644 index 000000000..89029dd70 --- /dev/null +++ b/Tests/JExtractSwiftTests/FFM/FFMSubscriptsTests.swift @@ -0,0 +1,212 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JExtractSwiftLib +import Testing + +import JExtractSwiftLib +import Testing + +@Suite +struct FFMSubscriptsTests { + private let noParamsSubscriptSource = """ + public struct MyStruct { + private var testVariable: Double = 0 + + public subscript() -> Double { + get { return testVariable } + set { testVariable = newValue } + } + } + """ + + private let subscriptWithParamsSource = """ + public struct MyStruct { + private var testVariable: [Int32] = [] + + public subscript(index: Int32) -> Int32 { + get { return testVariable[Int(index)] } + set { testVariable[Int(index)] = newValue } + } + } + """ + + @Test("Test generation of JavaClass for subscript with no parameters") + func generatesJavaClassForNoParams() throws { + try assertOutput(input: noParamsSubscriptSource, .ffm, .java, expectedChunks: [ + """ + private static class swiftjava_SwiftModule_MyStruct_subscript$get { + private static final FunctionDescriptor DESC = FunctionDescriptor.of( + /* -> */SwiftValueLayout.SWIFT_DOUBLE, + /* self: */SwiftValueLayout.SWIFT_POINTER + ); + """, + """ + private static final MemorySegment ADDR = + SwiftModule.findOrThrow("swiftjava_SwiftModule_MyStruct_subscript$get"); + """, + """ + private static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + public static double call(java.lang.foreign.MemorySegment self) { + try { + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall(self); + } + return (double) HANDLE.invokeExact(self); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + """, + """ + public double getSubscript() { + $ensureAlive(); + return swiftjava_SwiftModule_MyStruct_subscript$get.call(this.$memorySegment()); + """, + ]) + try assertOutput(input: noParamsSubscriptSource, .ffm, .java, expectedChunks: [ + """ + private static class swiftjava_SwiftModule_MyStruct_subscript$set { + private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + /* newValue: */SwiftValueLayout.SWIFT_DOUBLE, + /* self: */SwiftValueLayout.SWIFT_POINTER + ); + private static final MemorySegment ADDR = + SwiftModule.findOrThrow("swiftjava_SwiftModule_MyStruct_subscript$set"); + private static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + public static void call(double newValue, java.lang.foreign.MemorySegment self) { + try { + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall(newValue, self); + } + HANDLE.invokeExact(newValue, self); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + """, + """ + public void setSubscript(double newValue) { + $ensureAlive(); + swiftjava_SwiftModule_MyStruct_subscript$set.call(newValue, this.$memorySegment()); + """ + ]) + } + + @Test("Test generation of JavaClass for subscript with parameters") + func generatesJavaClassForParameters() throws { + try assertOutput(input: subscriptWithParamsSource, .ffm, .java, expectedChunks: [ + """ + private static class swiftjava_SwiftModule_MyStruct_subscript$get { + private static final FunctionDescriptor DESC = FunctionDescriptor.of( + /* -> */SwiftValueLayout.SWIFT_INT32, + /* index: */SwiftValueLayout.SWIFT_INT32, + /* self: */SwiftValueLayout.SWIFT_POINTER + ); + private static final MemorySegment ADDR = + SwiftModule.findOrThrow("swiftjava_SwiftModule_MyStruct_subscript$get"); + private static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + public static int call(int index, java.lang.foreign.MemorySegment self) { + try { + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall(index, self); + } + return (int) HANDLE.invokeExact(index, self); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + """, + """ + public int getSubscript(int index) { + $ensureAlive(); + return swiftjava_SwiftModule_MyStruct_subscript$get.call(index, this.$memorySegment()); + """, + ]) + try assertOutput(input: subscriptWithParamsSource, .ffm, .java, expectedChunks: [ + """ + private static class swiftjava_SwiftModule_MyStruct_subscript$set { + private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + /* index: */SwiftValueLayout.SWIFT_INT32, + /* newValue: */SwiftValueLayout.SWIFT_INT32, + /* self: */SwiftValueLayout.SWIFT_POINTER + ); + private static final MemorySegment ADDR = + SwiftModule.findOrThrow("swiftjava_SwiftModule_MyStruct_subscript$set"); + private static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + public static void call(int index, int newValue, java.lang.foreign.MemorySegment self) { + try { + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall(index, newValue, self); + } + HANDLE.invokeExact(index, newValue, self); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + """, + """ + public void setSubscript(int index, int newValue) { + $ensureAlive(); + swiftjava_SwiftModule_MyStruct_subscript$set.call(index, newValue, this.$memorySegment()); + """, + ]) + } + + @Test("Test generation of Swift thunks for subscript without parameters") + func subscriptWithoutParamsMethodSwiftThunk() throws { + try assertOutput( + input: noParamsSubscriptSource, + .ffm, + .swift, + expectedChunks: [ + """ + @_cdecl("swiftjava_SwiftModule_MyStruct_subscript$get") + public func swiftjava_SwiftModule_MyStruct_subscript$get(_ self: UnsafeRawPointer) -> Double { + return self.assumingMemoryBound(to: MyStruct.self).pointee[] + } + """, + """ + @_cdecl("swiftjava_SwiftModule_MyStruct_subscript$set") + public func swiftjava_SwiftModule_MyStruct_subscript$set(_ newValue: Double, _ self: UnsafeMutableRawPointer) { + self.assumingMemoryBound(to: MyStruct.self).pointee[] = newValue + } + """ + ] + ) + } + + @Test("Test generation of Swift thunks for subscript with parameters") + func subscriptWithParamsMethodSwiftThunk() throws { + try assertOutput( + input: subscriptWithParamsSource, + .ffm, + .swift, + expectedChunks: [ + """ + @_cdecl("swiftjava_SwiftModule_MyStruct_subscript$get") + public func swiftjava_SwiftModule_MyStruct_subscript$get(_ index: Int32, _ self: UnsafeRawPointer) -> Int32 { + return self.assumingMemoryBound(to: MyStruct.self).pointee[index] + } + """, + """ + @_cdecl("swiftjava_SwiftModule_MyStruct_subscript$set") + public func swiftjava_SwiftModule_MyStruct_subscript$set(_ index: Int32, _ newValue: Int32, _ self: UnsafeMutableRawPointer) { + self.assumingMemoryBound(to: MyStruct.self).pointee[index] = newValue + } + """ + ] + ) + } +} diff --git a/Tests/JExtractSwiftTests/FFMNestedTypesTests.swift b/Tests/JExtractSwiftTests/FFMNestedTypesTests.swift new file mode 100644 index 000000000..5bb422eaa --- /dev/null +++ b/Tests/JExtractSwiftTests/FFMNestedTypesTests.swift @@ -0,0 +1,56 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JExtractSwiftLib +import SwiftJavaConfigurationShared +import Testing + +final class FFMNestedTypesTests { + let class_interfaceFile = + """ + public enum MyNamespace { } + + extension MyNamespace { + public struct MyNestedStruct { + public func test() {} + } + } + """ + + @Test("Import: Nested type in extension MyNamespace { struct MyName {} }") + func test_nested_in_extension() throws { + var config = Configuration() + config.swiftModule = "__FakeModule" + let st = Swift2JavaTranslator(config: config) + st.log.logLevel = .error + + try st.analyze(path: "Fake.swift", text: class_interfaceFile) + + let generator = FFMSwift2JavaGenerator( + config: config, + translator: st, + javaPackage: "com.example.swift", + swiftOutputDirectory: "/fake", + javaOutputDirectory: "/fake" + ) + + guard let ty = st.importedTypes["MyNamespace.MyNestedStruct"] else { + fatalError("Didn't import nested type!") + } + + + + } + +} \ No newline at end of file diff --git a/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift b/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift index 457e3a7a0..79b51c197 100644 --- a/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift +++ b/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import JExtractSwiftLib +import SwiftJavaConfigurationShared import Testing final class FuncCallbackImportTests { @@ -31,20 +32,22 @@ final class FuncCallbackImportTests { public func callMe(callback: () -> Void) public func callMeMore(callback: (UnsafeRawPointer, Float) -> Int, fn: () -> ()) + public func withBuffer(body: (UnsafeRawBufferPointer) -> Int) """ @Test("Import: public func callMe(callback: () -> Void)") func func_callMeFunc_callback() throws { - let st = Swift2JavaTranslator( - swiftModuleName: "__FakeModule" - ) + var config = Configuration() + config.swiftModule = "__FakeModule" + let st = Swift2JavaTranslator(config: config) st.log.logLevel = .error - try st.analyze(file: "Fake.swift", text: Self.class_interfaceFile) + try st.analyze(path: "Fake.swift", text: Self.class_interfaceFile) let funcDecl = st.importedGlobalFuncs.first { $0.name == "callMe" }! let generator = FFMSwift2JavaGenerator( + config: config, translator: st, javaPackage: "com.example.swift", swiftOutputDirectory: "/fake", @@ -75,8 +78,8 @@ final class FuncCallbackImportTests { private static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); public static void call(java.lang.foreign.MemorySegment callback) { try { - if (SwiftKit.TRACE_DOWNCALLS) { - SwiftKit.traceDowncall(callback); + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall(callback); } HANDLE.invokeExact(callback); } catch (Throwable ex$) { @@ -94,7 +97,7 @@ final class FuncCallbackImportTests { void apply(); } private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid(); - private static final MethodHandle HANDLE = SwiftKit.upcallHandle(Function.class, "apply", DESC); + private static final MethodHandle HANDLE = SwiftRuntime.upcallHandle(Function.class, "apply", DESC); private static MemorySegment toUpcallStub(Function fi, Arena arena) { return Linker.nativeLinker().upcallStub(HANDLE.bindTo(fi), DESC, arena); } @@ -124,16 +127,16 @@ final class FuncCallbackImportTests { @Test("Import: public func callMeMore(callback: (UnsafeRawPointer, Float) -> Int, fn: () -> ())") func func_callMeMoreFunc_callback() throws { - let st = Swift2JavaTranslator( - swiftModuleName: "__FakeModule" - ) - st.log.logLevel = .error + var config = Configuration() + config.swiftModule = "__FakeModule" + let st = Swift2JavaTranslator(config: config) - try st.analyze(file: "Fake.swift", text: Self.class_interfaceFile) + try st.analyze(path: "Fake.swift", text: Self.class_interfaceFile) let funcDecl = st.importedGlobalFuncs.first { $0.name == "callMeMore" }! let generator = FFMSwift2JavaGenerator( + config: config, translator: st, javaPackage: "com.example.swift", swiftOutputDirectory: "/fake", @@ -165,8 +168,8 @@ final class FuncCallbackImportTests { private static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); public static void call(java.lang.foreign.MemorySegment callback, java.lang.foreign.MemorySegment fn) { try { - if (SwiftKit.TRACE_DOWNCALLS) { - SwiftKit.traceDowncall(callback, fn); + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall(callback, fn); } HANDLE.invokeExact(callback, fn); } catch (Throwable ex$) { @@ -188,7 +191,7 @@ final class FuncCallbackImportTests { /* _0: */SwiftValueLayout.SWIFT_POINTER, /* _1: */SwiftValueLayout.SWIFT_FLOAT ); - private static final MethodHandle HANDLE = SwiftKit.upcallHandle(Function.class, "apply", DESC); + private static final MethodHandle HANDLE = SwiftRuntime.upcallHandle(Function.class, "apply", DESC); private static MemorySegment toUpcallStub(Function fi, Arena arena) { return Linker.nativeLinker().upcallStub(HANDLE.bindTo(fi), DESC, arena); } @@ -204,7 +207,7 @@ final class FuncCallbackImportTests { void apply(); } private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid(); - private static final MethodHandle HANDLE = SwiftKit.upcallHandle(Function.class, "apply", DESC); + private static final MethodHandle HANDLE = SwiftRuntime.upcallHandle(Function.class, "apply", DESC); private static MemorySegment toUpcallStub(Function fi, Arena arena) { return Linker.nativeLinker().upcallStub(HANDLE.bindTo(fi), DESC, arena); } @@ -237,4 +240,101 @@ final class FuncCallbackImportTests { ) } + @Test("Import: public func withBuffer(body: (UnsafeRawBufferPointer) -> Int)") + func func_withBuffer_body() throws { + var config = Configuration() + config.swiftModule = "__FakeModule" + let st = Swift2JavaTranslator(config: config) + st.log.logLevel = .error + + try st.analyze(path: "Fake.swift", text: Self.class_interfaceFile) + + let funcDecl = st.importedGlobalFuncs.first { $0.name == "withBuffer" }! + + let generator = FFMSwift2JavaGenerator( + config: config, + translator: st, + javaPackage: "com.example.swift", + swiftOutputDirectory: "/fake", + javaOutputDirectory: "/fake" + ) + + let output = CodePrinter.toString { printer in + generator.printFunctionDowncallMethods(&printer, funcDecl) + } + + assertOutput( + output, + expected: + """ + // ==== -------------------------------------------------- + // withBuffer + /** + * {@snippet lang=c : + * void swiftjava___FakeModule_withBuffer_body(ptrdiff_t (*body)(const void *, ptrdiff_t)) + * } + */ + private static class swiftjava___FakeModule_withBuffer_body { + private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + /* body: */SwiftValueLayout.SWIFT_POINTER + ); + private static final MemorySegment ADDR = + __FakeModule.findOrThrow("swiftjava___FakeModule_withBuffer_body"); + private static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + public static void call(java.lang.foreign.MemorySegment body) { + try { + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall(body); + } + HANDLE.invokeExact(body); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + /** + * {snippet lang=c : + * ptrdiff_t (*)(const void *, ptrdiff_t) + * } + */ + private static class $body { + @FunctionalInterface + public interface Function { + long apply(java.lang.foreign.MemorySegment _0, long _1); + } + private static final FunctionDescriptor DESC = FunctionDescriptor.of( + /* -> */SwiftValueLayout.SWIFT_INT, + /* _0: */SwiftValueLayout.SWIFT_POINTER, + /* _1: */SwiftValueLayout.SWIFT_INT + ); + private static final MethodHandle HANDLE = SwiftRuntime.upcallHandle(Function.class, "apply", DESC); + private static MemorySegment toUpcallStub(Function fi, Arena arena) { + return Linker.nativeLinker().upcallStub(HANDLE.bindTo(fi), DESC, arena); + } + } + } + public static class withBuffer { + @FunctionalInterface + public interface body { + long apply(java.lang.foreign.MemorySegment _0); + } + private static MemorySegment $toUpcallStub(body fi, Arena arena) { + return swiftjava___FakeModule_withBuffer_body.$body.toUpcallStub((_0_pointer, _0_count) -> { + return fi.apply(_0_pointer.reinterpret(_0_count)); + }, arena); + } + } + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func withBuffer(body: (UnsafeRawBufferPointer) -> Int) + * } + */ + public static void withBuffer(withBuffer.body body) { + try(var arena$ = Arena.ofConfined()) { + swiftjava___FakeModule_withBuffer_body.call(withBuffer.$toUpcallStub(body, arena$)); + } + } + """ + ) + } } diff --git a/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift b/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift index 2cac62184..b6ae6f6c6 100644 --- a/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift +++ b/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import JExtractSwiftLib +import SwiftJavaConfigurationShared import Testing @Suite @@ -65,8 +66,8 @@ final class FunctionDescriptorTests { private static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); public static void call(long i) { try { - if (SwiftKit.TRACE_DOWNCALLS) { - SwiftKit.traceDowncall(i); + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall(i); } HANDLE.invokeExact(i); } catch (Throwable ex$) { @@ -101,8 +102,8 @@ final class FunctionDescriptorTests { private static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); public static void call(long l, int i32) { try { - if (SwiftKit.TRACE_DOWNCALLS) { - SwiftKit.traceDowncall(l, i32); + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall(l, i32); } HANDLE.invokeExact(l, i32); } catch (Throwable ex$) { @@ -137,8 +138,8 @@ final class FunctionDescriptorTests { private static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); public static long call(long i) { try { - if (SwiftKit.TRACE_DOWNCALLS) { - SwiftKit.traceDowncall(i); + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall(i); } return (long) HANDLE.invokeExact(i); } catch (Throwable ex$) { @@ -173,8 +174,8 @@ final class FunctionDescriptorTests { private static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); public static int call(java.lang.foreign.MemorySegment self) { try { - if (SwiftKit.TRACE_DOWNCALLS) { - SwiftKit.traceDowncall(self); + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall(self); } return (int) HANDLE.invokeExact(self); } catch (Throwable ex$) { @@ -208,8 +209,8 @@ final class FunctionDescriptorTests { private static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); public static void call(int newValue, java.lang.foreign.MemorySegment self) { try { - if (SwiftKit.TRACE_DOWNCALLS) { - SwiftKit.traceDowncall(newValue, self); + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall(newValue, self); } HANDLE.invokeExact(newValue, self); } catch (Throwable ex$) { @@ -233,18 +234,19 @@ extension FunctionDescriptorTests { logLevel: Logger.Level = .trace, body: (String) throws -> Void ) throws { - let st = Swift2JavaTranslator( - swiftModuleName: swiftModuleName - ) + var config = Configuration() + config.swiftModule = swiftModuleName + let st = Swift2JavaTranslator(config: config) st.log.logLevel = logLevel - try st.analyze(file: "/fake/Sample.swiftinterface", text: interfaceFile) + try st.analyze(path: "/fake/Sample.swiftinterface", text: interfaceFile) let funcDecl = st.importedGlobalFuncs.first { $0.name == methodIdentifier }! let generator = FFMSwift2JavaGenerator( + config: config, translator: st, javaPackage: javaPackage, swiftOutputDirectory: "/fake", @@ -266,14 +268,15 @@ extension FunctionDescriptorTests { logLevel: Logger.Level = .trace, body: (String) throws -> Void ) throws { - let st = Swift2JavaTranslator( - swiftModuleName: swiftModuleName - ) + var config = Configuration() + config.swiftModule = swiftModuleName + let st = Swift2JavaTranslator(config: config) st.log.logLevel = logLevel - try st.analyze(file: "/fake/Sample.swiftinterface", text: interfaceFile) + try st.analyze(path: "/fake/Sample.swiftinterface", text: interfaceFile) let generator = FFMSwift2JavaGenerator( + config: config, translator: st, javaPackage: javaPackage, swiftOutputDirectory: "/fake", diff --git a/Tests/JExtractSwiftTests/FunctionLoweringTests.swift b/Tests/JExtractSwiftTests/FunctionLoweringTests.swift index b7855e437..ba1aad064 100644 --- a/Tests/JExtractSwiftTests/FunctionLoweringTests.swift +++ b/Tests/JExtractSwiftTests/FunctionLoweringTests.swift @@ -315,6 +315,42 @@ final class FunctionLoweringTests { ) } + @Test("Lowering UnsafeRawBufferPointer") + func lowerRawBufferPointer() throws { + try assertLoweredFunction( + """ + func swapRawBufferPointer(buffer: UnsafeRawBufferPointer) -> UnsafeMutableRawBufferPointer {} + """, + expectedCDecl: """ + @_cdecl("c_swapRawBufferPointer") + public func c_swapRawBufferPointer(_ buffer_pointer: UnsafeRawPointer?, _ buffer_count: Int, _ _result_pointer: UnsafeMutablePointer, _ _result_count: UnsafeMutablePointer) { + let _result = swapRawBufferPointer(buffer: UnsafeRawBufferPointer(start: buffer_pointer, count: buffer_count)) + _result_pointer.initialize(to: _result.baseAddress) + _result_count.initialize(to: _result.count) + } + """, + expectedCFunction: "void c_swapRawBufferPointer(const void *buffer_pointer, ptrdiff_t buffer_count, void **_result_pointer, ptrdiff_t *_result_count)" + ) + } + + @Test("Lowering non-C-compatible closures") + func lowerComplexClosureParameter() throws { + try assertLoweredFunction( + """ + func withBuffer(body: (UnsafeRawBufferPointer) -> Int) {} + """, + expectedCDecl: """ + @_cdecl("c_withBuffer") + public func c_withBuffer(_ body: @convention(c) (UnsafeRawPointer?, Int) -> Int) { + withBuffer(body: { (_0) in + return body(_0.baseAddress, _0.count) + }) + } + """, + expectedCFunction: "void c_withBuffer(ptrdiff_t (*body)(const void *, ptrdiff_t))" + ) + } + @Test("Lowering () -> Void type") func lowerSimpleClosureTypes() throws { try assertLoweredFunction(""" @@ -347,6 +383,48 @@ final class FunctionLoweringTests { ) } + @Test("Lowering optional primitives") + func lowerOptionalParameters() throws { + try assertLoweredFunction( + """ + func fn(a1: Optional, a2: Int?, a3: Point?, a4: (some DataProtocol)?) + """, + sourceFile: """ + import Foundation + struct Point {} + """, + expectedCDecl:""" + @_cdecl("c_fn") + public func c_fn(_ a1: UnsafePointer?, _ a2: UnsafePointer?, _ a3: UnsafeRawPointer?, _ a4: UnsafeRawPointer?) { + fn(a1: a1?.pointee, a2: a2?.pointee, a3: a3?.assumingMemoryBound(to: Point.self).pointee, a4: a4?.assumingMemoryBound(to: Data.self).pointee) + } + """, + expectedCFunction: """ + void c_fn(const ptrdiff_t *a1, const ptrdiff_t *a2, const void *a3, const void *a4) + """) + } + + @Test("Lowering generic parameters") + func genericParam() throws { + try assertLoweredFunction( + """ + func fn(x: T, y: U?) where T: DataProtocol + """, + sourceFile: """ + import Foundation + """, + expectedCDecl: """ + @_cdecl("c_fn") + public func c_fn(_ x: UnsafeRawPointer, _ y: UnsafeRawPointer?) { + fn(x: x.assumingMemoryBound(to: Data.self).pointee, y: y?.assumingMemoryBound(to: Data.self).pointee) + } + """, + expectedCFunction: """ + void c_fn(const void *x, const void *y) + """ + ) + } + @Test("Lowering read accessor") func lowerGlobalReadAccessor() throws { try assertLoweredVariableAccessor( diff --git a/Tests/JExtractSwiftTests/InternalExtractTests.swift b/Tests/JExtractSwiftTests/InternalExtractTests.swift new file mode 100644 index 000000000..54829bd77 --- /dev/null +++ b/Tests/JExtractSwiftTests/InternalExtractTests.swift @@ -0,0 +1,49 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JExtractSwiftLib +import SwiftJavaConfigurationShared +import Testing + +final class InternalExtractTests { + let text = + """ + internal func catchMeIfYouCan() + """ + + @Test("Import: internal decl if configured") + func data_swiftThunk() throws { + var config = Configuration() + config.minimumInputAccessLevelMode = .internal + + try assertOutput( + input: text, + config: config, + .ffm, .java, + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * internal func catchMeIfYouCan() + * } + */ + public static void catchMeIfYouCan() { + swiftjava_SwiftModule_catchMeIfYouCan.call(); + } + """, + ] + ) + } +} \ No newline at end of file diff --git a/Tests/JExtractSwiftTests/JNI/JNIArrayTest.swift b/Tests/JExtractSwiftTests/JNI/JNIArrayTest.swift new file mode 100644 index 000000000..ebe3f807b --- /dev/null +++ b/Tests/JExtractSwiftTests/JNI/JNIArrayTest.swift @@ -0,0 +1,188 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JExtractSwiftLib +import Testing + +@Suite +struct JNIArrayTest { + + @Test("Import: (Array) -> Array (Java)") + func uint8Array_explicitType_java() throws { + try assertOutput( + input: "public func f(array: Array) -> Array {}", + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public static byte[] f(@Unsigned byte[] array) { + return SwiftModule.$f(Objects.requireNonNull(array, "array must not be null")); + } + """, + """ + private static native byte[] $f(byte[] array); + """, + ] + ) + } + + @Test("Import: (Array) -> Array (Swift)") + func uint8Array_explicitType_swift() throws { + try assertOutput( + input: "public func f(array: Array) -> Array {}", + .jni, .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024f___3B") + func Java_com_example_swift_SwiftModule__00024f___3B(environment: UnsafeMutablePointer!, thisClass: jclass, array: jbyteArray?) -> jbyteArray? { + return SwiftModule.f(array: [UInt8](fromJNI: array, in: environment)).getJNIValue(in: environment) + } + """ + ] + ) + } + + @Test("Import: ([UInt8]) -> [UInt8] (Java)") + func uint8Array_syntaxSugar_java() throws { + try assertOutput( + input: "public func f(array: Array) -> Array {}", + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public static byte[] f(@Unsigned byte[] array) { + return SwiftModule.$f(Objects.requireNonNull(array, "array must not be null")); + } + """, + """ + private static native byte[] $f(byte[] array); + """, + ] + ) + } + + @Test("Import: ([UInt8]) -> [UInt8] (Swift)") + func uint8Array_syntaxSugar_swift() throws { + try assertOutput( + input: "public func f(array: [UInt8]) -> [UInt8] {}", + .jni, .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024f___3B") + func Java_com_example_swift_SwiftModule__00024f___3B(environment: UnsafeMutablePointer!, thisClass: jclass, array: jbyteArray?) -> jbyteArray? { + return SwiftModule.f(array: [UInt8](fromJNI: array, in: environment)).getJNIValue(in: environment) + } + """ + ] + ) + } + + @Test("Import: ([Int64]) -> [Int64] (Java)") + func int64Array_syntaxSugar_java() throws { + try assertOutput( + input: "public func f(array: [Int64]) -> [Int64] {}", + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public static long[] f(long[] array) { + return SwiftModule.$f(Objects.requireNonNull(array, "array must not be null")); + } + """, + """ + private static native long[] $f(long[] array); + """, + ] + ) + } + + @Test("Import: ([Int64]) -> [Int64] (Swift)") + func int64Array_syntaxSugar_swift() throws { + try assertOutput( + input: "public func f(array: [Int64]) -> [Int64] {}", + .jni, .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024f___3J") + func Java_com_example_swift_SwiftModule__00024f___3J(environment: UnsafeMutablePointer!, thisClass: jclass, array: jlongArray?) -> jlongArray? { + return SwiftModule.f(array: [Int64](fromJNI: array, in: environment)).getJNIValue(in: environment) + } + """ + ] + ) + } + + @Test("Import: ([MySwiftClass]) -> [MySwiftClass] (Java)") + func swiftClassArray_syntaxSugar_java() throws { + try assertOutput( + input: """ + public class MySwiftClass {} + public func f(array: [MySwiftClass]) -> [MySwiftClass] {} + """, + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public static MySwiftClass[] f(MySwiftClass[] array, SwiftArena swiftArena$) { + return Arrays.stream(SwiftModule.$f(Arrays.stream(Objects.requireNonNull(array, "array must not be null")).mapToLong(MySwiftClass::$memoryAddress).toArray())).mapToObj((pointer) -> { + return MySwiftClass.wrapMemoryAddressUnsafe(pointer, swiftArena$); + } + ).toArray(MySwiftClass[]::new); + } + """, + """ + private static native long[] $f(long[] array); + """, + ] + ) + } + + @Test("Import: ([MySwiftClass]) -> [MySwiftClass] (Swift)") + func swiftClassArray_syntaxSugar_swift() throws { + try assertOutput( + input: """ + public class MySwiftClass {} + public func f(array: [MySwiftClass]) -> [MySwiftClass] {} + """, + .jni, .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024f___3J") + func Java_com_example_swift_SwiftModule__00024f___3J(environment: UnsafeMutablePointer!, thisClass: jclass, array: jlongArray?) -> jlongArray? { + return SwiftModule.f(array: [Int64](fromJNI: array, in: environment).map( { (pointer$) in + assert(pointer$ != 0, "pointer$ memory address was null") + let pointer$Bits$ = Int(pointer$) + let pointer$$ = UnsafeMutablePointer(bitPattern: pointer$Bits$) + guard let pointer$$ else { + fatalError("pointer$ memory address was null in call to \\(#function)!") + } + return pointer$$.pointee + } + )).map( { (object$) in + let object$$ = UnsafeMutablePointer.allocate(capacity: 1) + object$$.initialize(to: object$) + let object$Bits$ = Int64(Int(bitPattern: object$$)) + return object$Bits$ + } + ).getJNIValue(in: environment) + } + """ + ] + ) + } +} diff --git a/Tests/JExtractSwiftTests/JNI/JNIAsyncTests.swift b/Tests/JExtractSwiftTests/JNI/JNIAsyncTests.swift new file mode 100644 index 000000000..f246bd0f7 --- /dev/null +++ b/Tests/JExtractSwiftTests/JNI/JNIAsyncTests.swift @@ -0,0 +1,473 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JExtractSwiftLib +import Testing +import SwiftJavaConfigurationShared + +@Suite +struct JNIAsyncTests { + + @Test("Import: async -> Void (Java, CompletableFuture)") + func completableFuture_asyncVoid_java() throws { + try assertOutput( + input: "public func asyncVoid() async", + .jni, .java, + detectChunkByInitialLines: 2, + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func asyncVoid() async + * } + */ + public static java.util.concurrent.CompletableFuture asyncVoid() { + java.util.concurrent.CompletableFuture future$ = new java.util.concurrent.CompletableFuture(); + SwiftModule.$asyncVoid(future$); + return future$.thenApply((futureResult$) -> { + return futureResult$; + } + ); + } + """, + """ + private static native void $asyncVoid(java.util.concurrent.CompletableFuture result_future); + """, + ] + ) + } + + @Test("Import: async -> Void (Swift, CompletableFuture)") + func completableFuture_asyncVoid_swift() throws { + try assertOutput( + input: "public func asyncVoid() async", + .jni, .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024asyncVoid__Ljava_util_concurrent_CompletableFuture_2") + func Java_com_example_swift_SwiftModule__00024asyncVoid__Ljava_util_concurrent_CompletableFuture_2(environment: UnsafeMutablePointer!, thisClass: jclass, result_future: jobject?) { + let globalFuture = environment.interface.NewGlobalRef(environment, result_future) + var task: Task? = nil + #if swift(>=6.2) + if #available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, *) { + task = Task.immediate { + var environment = environment! + defer { + let deferEnvironment = try! JavaVirtualMachine.shared().environment() + deferEnvironment.interface.DeleteGlobalRef(deferEnvironment, globalFuture) + } + let swiftResult$ = await SwiftModule.asyncVoid() + environment = try! JavaVirtualMachine.shared().environment() + environment.interface.CallBooleanMethodA(environment, globalFuture, _JNIMethodIDCache.CompletableFuture.complete, [jvalue(l: nil)]) + } + } + #endif + if task == nil { + task = Task { + var environment = try! JavaVirtualMachine.shared().environment() + defer { + let deferEnvironment = try! JavaVirtualMachine.shared().environment() + deferEnvironment.interface.DeleteGlobalRef(deferEnvironment, globalFuture) + } + let swiftResult$ = await SwiftModule.asyncVoid() + environment = try! JavaVirtualMachine.shared().environment() + environment.interface.CallBooleanMethodA(environment, globalFuture, _JNIMethodIDCache.CompletableFuture.complete, [jvalue(l: nil)]) + } + } + } + """ + ] + ) + } + + @Test("Import: async throws -> Void (Java, CompletableFuture)") + func completableFuture_asyncThrowsVoid_java() throws { + try assertOutput( + input: "public func async() async throws", + .jni, .java, + detectChunkByInitialLines: 2, + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func async() async throws + * } + */ + public static java.util.concurrent.CompletableFuture async() { + java.util.concurrent.CompletableFuture future$ = new java.util.concurrent.CompletableFuture(); + SwiftModule.$async(future$); + return future$.thenApply((futureResult$) -> { + return futureResult$; + } + ); + } + """, + """ + private static native void $async(java.util.concurrent.CompletableFuture result_future); + """, + ] + ) + } + + @Test("Import: async throws -> Void (Swift, CompletableFuture)") + func completableFuture_asyncThrowsVoid_swift() throws { + try assertOutput( + input: "public func async() async throws", + .jni, .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024async__Ljava_util_concurrent_CompletableFuture_2") + func Java_com_example_swift_SwiftModule__00024async__Ljava_util_concurrent_CompletableFuture_2(environment: UnsafeMutablePointer!, thisClass: jclass, result_future: jobject?) { + let globalFuture = environment.interface.NewGlobalRef(environment, result_future) + var task: Task? = nil + #if swift(>=6.2) + if #available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, *) { + task = Task.immediate { + var environment = environment! + defer { + let deferEnvironment = try! JavaVirtualMachine.shared().environment() + deferEnvironment.interface.DeleteGlobalRef(deferEnvironment, globalFuture) + } + do { + let swiftResult$ = await try SwiftModule.async() + environment = try! JavaVirtualMachine.shared().environment() + environment.interface.CallBooleanMethodA(environment, globalFuture, _JNIMethodIDCache.CompletableFuture.complete, [jvalue(l: nil)]) + } + catch { + let catchEnvironment = try! JavaVirtualMachine.shared().environment() + let exception = catchEnvironment.interface.NewObjectA(catchEnvironment, _JNIMethodIDCache.Exception.class, _JNIMethodIDCache.Exception.constructWithMessage, [String(describing: error).getJValue(in: catchEnvironment)]) + catchEnvironment.interface.CallBooleanMethodA(catchEnvironment, globalFuture, _JNIMethodIDCache.CompletableFuture.completeExceptionally, [jvalue(l: exception)]) + } + } + } + #endif + if task == nil { + task = Task { + var environment = try! JavaVirtualMachine.shared().environment() + defer { + let deferEnvironment = try! JavaVirtualMachine.shared().environment() + deferEnvironment.interface.DeleteGlobalRef(deferEnvironment, globalFuture) + } + do { + let swiftResult$ = await try SwiftModule.async() + environment = try! JavaVirtualMachine.shared().environment() + environment.interface.CallBooleanMethodA(environment, globalFuture, _JNIMethodIDCache.CompletableFuture.complete, [jvalue(l: nil)]) + } + catch { + let catchEnvironment = try! JavaVirtualMachine.shared().environment() + let exception = catchEnvironment.interface.NewObjectA(catchEnvironment, _JNIMethodIDCache.Exception.class, _JNIMethodIDCache.Exception.constructWithMessage, [String(describing: error).getJValue(in: catchEnvironment)]) + catchEnvironment.interface.CallBooleanMethodA(catchEnvironment, globalFuture, _JNIMethodIDCache.CompletableFuture.completeExceptionally, [jvalue(l: exception)]) + } + } + } + } + """ + ] + ) + } + + @Test("Import: (Int64) async -> Int64 (Java, CompletableFuture)") + func completableFuture_asyncIntToInt_java() throws { + try assertOutput( + input: "public func async(i: Int64) async -> Int64", + .jni, .java, + detectChunkByInitialLines: 2, + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func async(i: Int64) async -> Int64 + * } + */ + public static java.util.concurrent.CompletableFuture async(long i) { + java.util.concurrent.CompletableFuture future$ = new java.util.concurrent.CompletableFuture(); + SwiftModule.$async(i, future$); + return future$.thenApply((futureResult$) -> { + return futureResult$; + } + ); + } + """, + """ + private static native void $async(long i, java.util.concurrent.CompletableFuture result_future); + """, + ] + ) + } + + @Test("Import: (Int64) async -> Int64 (Swift, CompletableFuture)") + func completableFuture_asyncIntToInt_swift() throws { + try assertOutput( + input: "public func async(i: Int64) async -> Int64", + .jni, .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024async__JLjava_util_concurrent_CompletableFuture_2") + func Java_com_example_swift_SwiftModule__00024async__JLjava_util_concurrent_CompletableFuture_2(environment: UnsafeMutablePointer!, thisClass: jclass, i: jlong, result_future: jobject?) { + let globalFuture = environment.interface.NewGlobalRef(environment, result_future) + var task: Task? = nil + #if swift(>=6.2) + if #available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, *) { + task = Task.immediate { + var environment = environment! + defer { + let deferEnvironment = try! JavaVirtualMachine.shared().environment() + deferEnvironment.interface.DeleteGlobalRef(deferEnvironment, globalFuture) + } + let swiftResult$ = await SwiftModule.async(i: Int64(fromJNI: i, in: environment)) + environment = try! JavaVirtualMachine.shared().environment() + let boxedResult$ = SwiftJavaRuntimeSupport._JNIBoxedConversions.box(swiftResult$.getJNIValue(in: environment), in: environment) + environment.interface.CallBooleanMethodA(environment, globalFuture, _JNIMethodIDCache.CompletableFuture.complete, [jvalue(l: boxedResult$)]) + } + } + #endif // end of swift(>=6.2) + if task == nil { + task = Task { + var environment = try! JavaVirtualMachine.shared().environment() + defer { + let deferEnvironment = try! JavaVirtualMachine.shared().environment() + deferEnvironment.interface.DeleteGlobalRef(deferEnvironment, globalFuture) + } + let swiftResult$ = await SwiftModule.async(i: Int64(fromJNI: i, in: environment)) + environment = try! JavaVirtualMachine.shared().environment() + let boxedResult$ = SwiftJavaRuntimeSupport._JNIBoxedConversions.box(swiftResult$.getJNIValue(in: environment), in: environment) + environment.interface.CallBooleanMethodA(environment, globalFuture, _JNIMethodIDCache.CompletableFuture.complete, [jvalue(l: boxedResult$)]) + } + } + return + } + """ + ] + ) + } + + @Test("Import: (MyClass) async -> MyClass (Java, CompletableFuture)") + func completableFuture_asyncMyClassToMyClass_java() throws { + try assertOutput( + input: """ + class MyClass { } + + public func async(c: MyClass) async -> MyClass + """, + .jni, .java, + detectChunkByInitialLines: 2, + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func async(c: MyClass) async -> MyClass + * } + */ + public static java.util.concurrent.CompletableFuture async(MyClass c, SwiftArena swiftArena$) { + java.util.concurrent.CompletableFuture future$ = new java.util.concurrent.CompletableFuture(); + SwiftModule.$async(c.$memoryAddress(), future$); + return future$.thenApply((futureResult$) -> { + return MyClass.wrapMemoryAddressUnsafe(futureResult$, swiftArena$); + } + ); + } + """, + """ + private static native void $async(long c, java.util.concurrent.CompletableFuture result_future); + """, + ] + ) + } + + @Test("Import: (MyClass) async -> MyClass (Swift, CompletableFuture)") + func completableFuture_asyncMyClassToMyClass_swift() throws { + try assertOutput( + input: """ + class MyClass { } + + public func async(c: MyClass) async -> MyClass + """, + .jni, .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024async__JLjava_util_concurrent_CompletableFuture_2") + func Java_com_example_swift_SwiftModule__00024async__JLjava_util_concurrent_CompletableFuture_2(environment: UnsafeMutablePointer!, thisClass: jclass, c: jlong, result_future: jobject?) { + assert(c != 0, "c memory address was null") + let cBits$ = Int(Int64(fromJNI: c, in: environment)) + let c$ = UnsafeMutablePointer(bitPattern: cBits$) + guard let c$ else { + fatalError("c memory address was null in call to \\(#function)!") + } + let globalFuture = environment.interface.NewGlobalRef(environment, result_future) + var task: Task? = nil + #if swift(>=6.2) + if #available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, *) { + task = Task.immediate { + var environment = environment! + defer { + let deferEnvironment = try! JavaVirtualMachine.shared().environment() + deferEnvironment.interface.DeleteGlobalRef(deferEnvironment, globalFuture) + } + let swiftResult$ = await SwiftModule.async(c: c$.pointee) + environment = try! JavaVirtualMachine.shared().environment() + let result$ = UnsafeMutablePointer.allocate(capacity: 1) + result$.initialize(to: swiftResult$) + let resultBits$ = Int64(Int(bitPattern: result$)) + let boxedResult$ = SwiftJavaRuntimeSupport._JNIBoxedConversions.box(resultBits$.getJNIValue(in: environment), in: environment) + environment.interface.CallBooleanMethodA(environment, globalFuture, _JNIMethodIDCache.CompletableFuture.complete, [jvalue(l: boxedResult$)]) + } + } + #endif + if task == nil { + task = Task { + var environment = try! JavaVirtualMachine.shared().environment() + defer { + let deferEnvironment = try! JavaVirtualMachine.shared().environment() + deferEnvironment.interface.DeleteGlobalRef(deferEnvironment, globalFuture) + } + let swiftResult$ = await SwiftModule.async(c: c$.pointee) + environment = try! JavaVirtualMachine.shared().environment() + let result$ = UnsafeMutablePointer.allocate(capacity: 1) + result$.initialize(to: swiftResult$) + let resultBits$ = Int64(Int(bitPattern: result$)) + let boxedResult$ = SwiftJavaRuntimeSupport._JNIBoxedConversions.box(resultBits$.getJNIValue(in: environment), in: environment) + environment.interface.CallBooleanMethodA(environment, globalFuture, _JNIMethodIDCache.CompletableFuture.complete, [jvalue(l: boxedResult$)]) + } + } + return + } + """ + ] + ) + } + + @Test("Import: (String) async -> String (Java, CompletableFuture)") + func completableFuture_asyncStringToString_java() throws { + try assertOutput( + input: """ + public func async(s: String) async -> String + """, + .jni, .java, + detectChunkByInitialLines: 2, + expectedChunks: [ + """ + public static java.util.concurrent.CompletableFuture async(java.lang.String s) { + java.util.concurrent.CompletableFuture future$ = new java.util.concurrent.CompletableFuture(); + SwiftModule.$async(s, future$); + return future$.thenApply((futureResult$) -> { + return futureResult$; + } + ); + } + """, + """ + private static native void $async(java.lang.String s, java.util.concurrent.CompletableFuture result_future); + """, + ] + ) + } + + @Test("Import: (String) async -> String (Swift, CompletableFuture)") + func completableFuture_asyncStringToString_swift() throws { + try assertOutput( + input: """ + public func async(s: String) async -> String + """, + .jni, .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024async__Ljava_lang_String_2Ljava_util_concurrent_CompletableFuture_2") + func Java_com_example_swift_SwiftModule__00024async__Ljava_lang_String_2Ljava_util_concurrent_CompletableFuture_2(environment: UnsafeMutablePointer!, thisClass: jclass, s: jstring?, result_future: jobject?) { + let s = environment.interface.NewGlobalRef(environment, s) + let globalFuture = environment.interface.NewGlobalRef(environment, result_future) + ... + defer { + let deferEnvironment = try! JavaVirtualMachine.shared().environment() + deferEnvironment.interface.DeleteGlobalRef(deferEnvironment, globalFuture) + deferEnvironment.interface.DeleteGlobalRef(deferEnvironment, s) + } + ... + environment.interface.CallBooleanMethodA(environment, globalFuture, _JNIMethodIDCache.CompletableFuture.complete, [jvalue(l: swiftResult$.getJNIValue(in: environment))]) + ... + } + """ + ] + ) + } + + @Test("Import: (MyClass) async -> MyClass (Java, LegacyFuture)") + func legacyFuture_asyncMyClassToMyClass_java() throws { + var config = Configuration() + config.asyncFuncMode = .legacyFuture + + try assertOutput( + input: """ + class MyClass { } + + public func async(c: MyClass) async -> MyClass + """, + config: config, + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public static java.util.concurrent.Future async(MyClass c, SwiftArena swiftArena$) { + org.swift.swiftkit.core.SimpleCompletableFuture future$ = new org.swift.swiftkit.core.SimpleCompletableFuture(); + SwiftModule.$async(c.$memoryAddress(), future$); + return future$.thenApply((futureResult$) -> { + return MyClass.wrapMemoryAddressUnsafe(futureResult$, swiftArena$); + } + ); + } + """, + """ + private static native void $async(long c, org.swift.swiftkit.core.SimpleCompletableFuture result_future); + """, + ] + ) + } + + @Test("Import: (MyClass) async -> MyClass (Swift, LegacyFuture)") + func legacyFuture_asyncMyClassToMyClass_swift() throws { + var config = Configuration() + config.asyncFuncMode = .legacyFuture + + try assertOutput( + input: """ + class MyClass { } + + public func async(c: MyClass) async -> MyClass + """, + config: config, + .jni, .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024async__JLorg_swift_swiftkit_core_SimpleCompletableFuture_2") + func Java_com_example_swift_SwiftModule__00024async__JLorg_swift_swiftkit_core_SimpleCompletableFuture_2(environment: UnsafeMutablePointer!, thisClass: jclass, c: jlong, result_future: jobject?) { + ... + var task: Task? = nil + ... + environment.interface.CallBooleanMethodA(environment, globalFuture, _JNIMethodIDCache.SimpleCompletableFuture.complete, [jvalue(l: boxedResult$)]) + ... + } + """ + ] + ) + } +} diff --git a/Tests/JExtractSwiftTests/JNI/JNIClassTests.swift b/Tests/JExtractSwiftTests/JNI/JNIClassTests.swift new file mode 100644 index 000000000..c3102a623 --- /dev/null +++ b/Tests/JExtractSwiftTests/JNI/JNIClassTests.swift @@ -0,0 +1,424 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JExtractSwiftLib +import Testing + +@Suite +struct JNIClassTests { + let source = """ + public class MyClass { + let x: Int64 + let y: Int64 + + public static func method() {} + + public init(x: Int64, y: Int64) { + self.x = y + self.y = y + } + + public init() { + self.x = 0 + self.y = 0 + } + + public func doSomething(x: Int64) {} + + public func copy() -> MyClass {} + public func isEqual(to other: MyClass) -> Bool {} + } + """ + + @Test + func generatesJavaClass() throws { + try assertOutput( + input: source, + .jni, .java, + expectedChunks: [ + """ + // Generated by jextract-swift + // Swift module: SwiftModule + + package com.example.swift; + + import org.swift.swiftkit.core.*; + import org.swift.swiftkit.core.util.*; + import java.util.*; + import java.util.concurrent.atomic.AtomicBoolean; + import org.swift.swiftkit.core.annotations.*; + """, + """ + public final class MyClass implements JNISwiftInstance { + static final String LIB_NAME = "SwiftModule"; + + @SuppressWarnings("unused") + private static final boolean INITIALIZED_LIBS = initializeLibs(); + static boolean initializeLibs() { + System.loadLibrary(LIB_NAME); + return true; + } + """, + """ + private final long selfPointer; + """, + """ + public long $memoryAddress() { + return this.selfPointer; + } + """, + """ + private MyClass(long selfPointer, SwiftArena swiftArena) { + SwiftObjects.requireNonZero(selfPointer, "selfPointer"); + this.selfPointer = selfPointer; + + // Only register once we have fully initialized the object since this will need the object pointer. + swiftArena.register(this); + } + """, + """ + public static MyClass wrapMemoryAddressUnsafe(long selfPointer, SwiftArena swiftArena) { + return new MyClass(selfPointer, swiftArena); + } + """ + ]) + try assertOutput( + input: source, + .jni, .java, + expectedChunks: [ + """ + private static native void $destroy(long selfPointer); + """ + ]) + try assertOutput( + input: source, + .jni, .java, + expectedChunks: [ + """ + @Override + public Runnable $createDestroyFunction() { + long self$ = this.$memoryAddress(); + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall("MyClass.$createDestroyFunction", + "this", this, + "self", self$); + } + return new Runnable() { + @Override + public void run() { + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall("MyClass.$destroy", "self", self$); + } + MyClass.$destroy(self$); + } + }; + """ + ]) + } + + @Test + func staticMethod_javaBindings() throws { + try assertOutput( + input: source, + .jni, + .java, + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public static func method() + * } + */ + public static void method() { + MyClass.$method(); + } + """, + """ + private static native void $method(); + """, + ] + ) + } + + @Test + func staticMethod_swiftThunks() throws { + try assertOutput( + input: source, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_MyClass__00024method__") + func Java_com_example_swift_MyClass__00024method__(environment: UnsafeMutablePointer!, thisClass: jclass) { + MyClass.method() + } + """ + ] + ) + } + + @Test + func initializer_javaBindings() throws { + try assertOutput( + input: source, + .jni, + .java, + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public init(x: Int64, y: Int64) + * } + */ + public static MyClass init(long x, long y, SwiftArena swiftArena$) { + return MyClass.wrapMemoryAddressUnsafe(MyClass.$init(x, y), swiftArena$); + } + """, + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public init() + * } + */ + public static MyClass init(SwiftArena swiftArena$) { + return MyClass.wrapMemoryAddressUnsafe(MyClass.$init(), swiftArena$); + } + """, + """ + private static native long $init(long x, long y); + """, + """ + private static native long $init(); + """, + ] + ) + } + + @Test + func initializer_swiftThunks() throws { + try assertOutput( + input: source, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_MyClass__00024init__JJ") + func Java_com_example_swift_MyClass__00024init__JJ(environment: UnsafeMutablePointer!, thisClass: jclass, x: jlong, y: jlong) -> jlong { + let result$ = UnsafeMutablePointer.allocate(capacity: 1) + result$.initialize(to: MyClass.init(x: Int64(fromJNI: x, in: environment), y: Int64(fromJNI: y, in: environment))) + let resultBits$ = Int64(Int(bitPattern: result$)) + return resultBits$.getJNIValue(in: environment) + } + """, + """ + @_cdecl("Java_com_example_swift_MyClass__00024init__") + func Java_com_example_swift_MyClass__00024init__(environment: UnsafeMutablePointer!, thisClass: jclass) -> jlong { + let result$ = UnsafeMutablePointer.allocate(capacity: 1) + result$.initialize(to: MyClass.init()) + let resultBits$ = Int64(Int(bitPattern: result$)) + return resultBits$.getJNIValue(in: environment) + } + """, + ] + ) + } + + @Test + func destroyFunction_swiftThunks() throws { + try assertOutput( + input: source, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_MyClass__00024destroy__J") + func Java_com_example_swift_MyClass__00024destroy__J(environment: UnsafeMutablePointer!, thisClass: jclass, selfPointer: jlong) { + guard let env$ = environment else { + fatalError("Missing JNIEnv in downcall to \\(#function)") + } + assert(selfPointer != 0, "selfPointer memory address was null") + let selfBits$ = Int(Int64(fromJNI: selfPointer, in: env$)) + guard let self$ = UnsafeMutablePointer(bitPattern: selfBits$) else { + fatalError("self memory address was null in call to \\(#function)!") + } + self$.deinitialize(count: 1) + self$.deallocate() + } + """ + ] + ) + } + + @Test + func memberMethod_javaBindings() throws { + try assertOutput( + input: source, + .jni, + .java, + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func doSomething(x: Int64) + * } + */ + public void doSomething(long x) { + MyClass.$doSomething(x, this.$memoryAddress()); + } + """, + """ + private static native void $doSomething(long x, long self); + """, + ] + ) + } + + @Test + func memberMethod_swiftThunks() throws { + try assertOutput( + input: source, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_MyClass__00024doSomething__JJ") + func Java_com_example_swift_MyClass__00024doSomething__JJ(environment: UnsafeMutablePointer!, thisClass: jclass, x: jlong, self: jlong) { + assert(self != 0, "self memory address was null") + let selfBits$ = Int(Int64(fromJNI: self, in: environment)) + let self$ = UnsafeMutablePointer(bitPattern: selfBits$) + guard let self$ else { + fatalError("self memory address was null in call to \\(#function)!") + } + self$.pointee.doSomething(x: Int64(fromJNI: x, in: environment)) + } + """ + ] + ) + } + + @Test + func methodReturningClass_javaBindings() throws { + try assertOutput( + input: source, + .jni, + .java, + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func copy() -> MyClass + * } + */ + public MyClass copy(SwiftArena swiftArena$) { + return MyClass.wrapMemoryAddressUnsafe(MyClass.$copy(this.$memoryAddress()), swiftArena$); + } + """, + """ + private static native long $copy(long self); + """, + ] + ) + } + + @Test + func methodReturningClass_swiftThunks() throws { + try assertOutput( + input: source, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_MyClass__00024copy__J") + func Java_com_example_swift_MyClass__00024copy__J(environment: UnsafeMutablePointer!, thisClass: jclass, self: jlong) -> jlong { + assert(self != 0, "self memory address was null") + let selfBits$ = Int(Int64(fromJNI: self, in: environment)) + let self$ = UnsafeMutablePointer(bitPattern: selfBits$) + guard let self$ else { + fatalError("self memory address was null in call to \\(#function)!") + } + let result$ = UnsafeMutablePointer.allocate(capacity: 1) + result$.initialize(to: self$.pointee.copy()) + let resultBits$ = Int64(Int(bitPattern: result$)) + return resultBits$.getJNIValue(in: environment) + } + """ + ] + ) + } + + @Test + func classAsParameter_javaBindings() throws { + try assertOutput( + input: source, + .jni, + .java, + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func isEqual(to other: MyClass) -> Bool + * } + */ + public boolean isEqual(MyClass other) { + return MyClass.$isEqual(other.$memoryAddress(), this.$memoryAddress()); + } + """, + """ + private static native boolean $isEqual(long other, long self); + """, + ] + ) + } + + @Test + func classAsParameter_swiftThunks() throws { + try assertOutput( + input: source, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_MyClass__00024isEqual__JJ") + func Java_com_example_swift_MyClass__00024isEqual__JJ(environment: UnsafeMutablePointer!, thisClass: jclass, other: jlong, self: jlong) -> jboolean { + assert(other != 0, "other memory address was null") + let otherBits$ = Int(Int64(fromJNI: other, in: environment)) + let other$ = UnsafeMutablePointer(bitPattern: otherBits$) + guard let other$ else { + fatalError("other memory address was null in call to \\(#function)!") + } + assert(self != 0, "self memory address was null") + let selfBits$ = Int(Int64(fromJNI: self, in: environment)) + let self$ = UnsafeMutablePointer(bitPattern: selfBits$) + guard let self$ else { + fatalError("self memory address was null in call to \\(#function)!") + } + return self$.pointee.isEqual(to: other$.pointee).getJNIValue(in: environment) + } + """ + ] + ) + } +} diff --git a/Tests/JExtractSwiftTests/JNI/JNIClosureTests.swift b/Tests/JExtractSwiftTests/JNI/JNIClosureTests.swift new file mode 100644 index 000000000..b54749c13 --- /dev/null +++ b/Tests/JExtractSwiftTests/JNI/JNIClosureTests.swift @@ -0,0 +1,129 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JExtractSwiftLib +import Testing + +@Suite +struct JNIClosureTests { + let source = + """ + public func emptyClosure(closure: () -> ()) {} + public func closureWithArgumentsAndReturn(closure: (Int64, Bool) -> Int64) {} + """ + + @Test + func emptyClosure_javaBindings() throws { + try assertOutput(input: source, .jni, .java, expectedChunks: [ + """ + public static class emptyClosure { + @FunctionalInterface + public interface closure { + void apply(); + } + } + """, + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func emptyClosure(closure: () -> ()) + * } + */ + public static void emptyClosure(com.example.swift.SwiftModule.emptyClosure.closure closure) { + SwiftModule.$emptyClosure(closure); + } + """, + """ + private static native void $emptyClosure(com.example.swift.SwiftModule.emptyClosure.closure closure); + """ + ]) + } + + @Test + func emptyClosure_swiftThunks() throws { + try assertOutput( + input: source, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024emptyClosure__Lcom_example_swift_SwiftModule_00024emptyClosure_00024closure_2") + func Java_com_example_swift_SwiftModule__00024emptyClosure__Lcom_example_swift_SwiftModule_00024emptyClosure_00024closure_2(environment: UnsafeMutablePointer!, thisClass: jclass, closure: jobject?) { + SwiftModule.emptyClosure(closure: { + let class$ = environment.interface.GetObjectClass(environment, closure) + let methodID$ = environment.interface.GetMethodID(environment, class$, "apply", "()V")! + let arguments$: [jvalue] = [] + environment.interface.CallVoidMethodA(environment, closure, methodID$, arguments$) + } + ) + } + """ + ] + ) + } + + @Test + func closureWithArgumentsAndReturn_javaBindings() throws { + try assertOutput(input: source, .jni, .java, expectedChunks: [ + """ + public static class closureWithArgumentsAndReturn { + @FunctionalInterface + public interface closure { + long apply(long _0, boolean _1); + } + } + """, + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func closureWithArgumentsAndReturn(closure: (Int64, Bool) -> Int64) + * } + */ + public static void closureWithArgumentsAndReturn(com.example.swift.SwiftModule.closureWithArgumentsAndReturn.closure closure) { + SwiftModule.$closureWithArgumentsAndReturn(closure); + } + """, + """ + private static native void $closureWithArgumentsAndReturn(com.example.swift.SwiftModule.closureWithArgumentsAndReturn.closure closure); + """ + ]) + } + + @Test + func closureWithArgumentsAndReturn_swiftThunks() throws { + try assertOutput( + input: source, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024closureWithArgumentsAndReturn__Lcom_example_swift_SwiftModule_00024closureWithArgumentsAndReturn_00024closure_2") + func Java_com_example_swift_SwiftModule__00024closureWithArgumentsAndReturn__Lcom_example_swift_SwiftModule_00024closureWithArgumentsAndReturn_00024closure_2(environment: UnsafeMutablePointer!, thisClass: jclass, closure: jobject?) { + SwiftModule.closureWithArgumentsAndReturn(closure: { _0, _1 in + let class$ = environment.interface.GetObjectClass(environment, closure) + let methodID$ = environment.interface.GetMethodID(environment, class$, "apply", "(JZ)J")! + let arguments$: [jvalue] = [_0.getJValue(in: environment), _1.getJValue(in: environment)] + return Int64(fromJNI: environment.interface.CallLongMethodA(environment, closure, methodID$, arguments$), in: environment) + } + ) + } + """ + ] + ) + } +} diff --git a/Tests/JExtractSwiftTests/JNI/JNIEnumTests.swift b/Tests/JExtractSwiftTests/JNI/JNIEnumTests.swift new file mode 100644 index 000000000..2188bcd62 --- /dev/null +++ b/Tests/JExtractSwiftTests/JNI/JNIEnumTests.swift @@ -0,0 +1,326 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JExtractSwiftLib +import Testing + +@Suite +struct JNIEnumTests { + let source = """ + public enum MyEnum { + case first + case second(String) + case third(x: Int64, y: Int32) + } + """ + + @Test + func generatesJavaClass() throws { + try assertOutput( + input: source, + .jni, .java, + expectedChunks: [ + """ + // Generated by jextract-swift + // Swift module: SwiftModule + + package com.example.swift; + + import org.swift.swiftkit.core.*; + import org.swift.swiftkit.core.util.*; + import java.util.*; + import java.util.concurrent.atomic.AtomicBoolean; + import org.swift.swiftkit.core.annotations.*; + """, + """ + public final class MyEnum implements JNISwiftInstance { + static final String LIB_NAME = "SwiftModule"; + + @SuppressWarnings("unused") + private static final boolean INITIALIZED_LIBS = initializeLibs(); + static boolean initializeLibs() { + System.loadLibrary(LIB_NAME); + return true; + } + """, + """ + private MyEnum(long selfPointer, SwiftArena swiftArena) { + SwiftObjects.requireNonZero(selfPointer, "selfPointer"); + this.selfPointer = selfPointer; + + // Only register once we have fully initialized the object since this will need the object pointer. + swiftArena.register(this); + } + """, + """ + private final long selfPointer; + """, + """ + public long $memoryAddress() { + return this.selfPointer; + } + """, + """ + public static MyEnum wrapMemoryAddressUnsafe(long selfPointer, SwiftArena swiftArena) { + return new MyEnum(selfPointer, swiftArena); + } + """, + """ + private static native void $destroy(long selfPointer); + """, + """ + @Override + public Runnable $createDestroyFunction() { + long self$ = this.$memoryAddress(); + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall("MyEnum.$createDestroyFunction", + "this", this, + "self", self$); + } + return new Runnable() { + @Override + public void run() { + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall("MyEnum.$destroy", "self", self$); + } + MyEnum.$destroy(self$); + } + }; + """ + ]) + } + + @Test + func generatesDiscriminator_java() throws { + try assertOutput( + input: source, + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public enum Discriminator { + FIRST, + SECOND, + THIRD + } + """, + """ + public Discriminator getDiscriminator() { + return Discriminator.values()[$getDiscriminator(this.$memoryAddress())]; + } + """, + """ + private static native int $getDiscriminator(long self); + """ + ]) + } + + @Test + func generatesDiscriminator_swift() throws { + try assertOutput( + input: source, + .jni, .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_MyEnum__00024getDiscriminator__J") + func Java_com_example_swift_MyEnum__00024getDiscriminator__J(environment: UnsafeMutablePointer!, thisClass: jclass, selfPointer: jlong) -> jint { + ... + switch (self$.pointee) { + case .first: return 0 + case .second: return 1 + case .third: return 2 + } + } + """ + ]) + } + + @Test + func generatesCases_java() throws { + try assertOutput( + input: source, + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public sealed interface Case {} + """, + """ + public Case getCase() { + Discriminator discriminator = this.getDiscriminator(); + switch (discriminator) { + case FIRST: return this.getAsFirst().orElseThrow(); + case SECOND: return this.getAsSecond().orElseThrow(); + case THIRD: return this.getAsThird().orElseThrow(); + } + throw new RuntimeException("Unknown discriminator value " + discriminator); + } + """, + """ + public record First() implements Case { + record _NativeParameters() {} + } + """, + """ + public record Second(java.lang.String arg0) implements Case { + record _NativeParameters(java.lang.String arg0) {} + } + """, + """ + public record Third(long x, int y) implements Case { + record _NativeParameters(long x, int y) {} + } + """ + ]) + } + + @Test + func generatesCaseInitializers_java() throws { + try assertOutput( + input: source, + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public static MyEnum first(SwiftArena swiftArena$) { + return MyEnum.wrapMemoryAddressUnsafe(MyEnum.$first(), swiftArena$); + } + """, + """ + public static MyEnum second(java.lang.String arg0, SwiftArena swiftArena$) { + return MyEnum.wrapMemoryAddressUnsafe(MyEnum.$second(arg0), swiftArena$); + } + """, + """ + public static MyEnum third(long x, int y, SwiftArena swiftArena$) { + return MyEnum.wrapMemoryAddressUnsafe(MyEnum.$third(x, y), swiftArena$); + } + """ + ]) + } + + @Test + func generatesCaseInitializers_swift() throws { + try assertOutput( + input: source, + .jni, .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_MyEnum__00024first__") + func Java_com_example_swift_MyEnum__00024first__(environment: UnsafeMutablePointer!, thisClass: jclass) -> jlong { + let result$ = UnsafeMutablePointer.allocate(capacity: 1) + result$.initialize(to: MyEnum.first) + let resultBits$ = Int64(Int(bitPattern: result$)) + return resultBits$.getJNIValue(in: environment) + } + """, + """ + @_cdecl("Java_com_example_swift_MyEnum__00024second__Ljava_lang_String_2") + func Java_com_example_swift_MyEnum__00024second__Ljava_lang_String_2(environment: UnsafeMutablePointer!, thisClass: jclass, arg0: jstring?) -> jlong { + let result$ = UnsafeMutablePointer.allocate(capacity: 1) + result$.initialize(to: MyEnum.second(String(fromJNI: arg0, in: environment))) + let resultBits$ = Int64(Int(bitPattern: result$)) + return resultBits$.getJNIValue(in: environment) + } + """, + """ + @_cdecl("Java_com_example_swift_MyEnum__00024third__JI") + func Java_com_example_swift_MyEnum__00024third__JI(environment: UnsafeMutablePointer!, thisClass: jclass, x: jlong, y: jint) -> jlong { + let result$ = UnsafeMutablePointer.allocate(capacity: 1) + result$.initialize(to: MyEnum.third(x: Int64(fromJNI: x, in: environment), y: Int32(fromJNI: y, in: environment))) + let resultBits$ = Int64(Int(bitPattern: result$)) + return resultBits$.getJNIValue(in: environment) + } + """ + ]) + } + + @Test + func generatesGetAsCase_java() throws { + try assertOutput( + input: source, + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public Optional getAsFirst() { + if (getDiscriminator() != Discriminator.FIRST) { + return Optional.empty(); + } + return Optional.of(new First()); + } + """, + """ + public Optional getAsSecond() { + if (getDiscriminator() != Discriminator.SECOND) { + return Optional.empty(); + } + Second._NativeParameters $nativeParameters = MyEnum.$getAsSecond(this.$memoryAddress()); + return Optional.of(new Second($nativeParameters.arg0)); + } + """, + """ + public Optional getAsThird() { + if (getDiscriminator() != Discriminator.THIRD) { + return Optional.empty(); + } + Third._NativeParameters $nativeParameters = MyEnum.$getAsThird(this.$memoryAddress()); + return Optional.of(new Third($nativeParameters.x, $nativeParameters.y)); + } + """ + ]) + } + + @Test + func generatesGetAsCase_swift() throws { + try assertOutput( + input: source, + .jni, .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_MyEnum__00024getAsSecond__J") + func Java_com_example_swift_MyEnum__00024getAsSecond__J(environment: UnsafeMutablePointer!, thisClass: jclass, self: jlong) -> jobject? { + ... + guard case .second(let _0) = self$.pointee else { + fatalError("Expected enum case 'second', but was '\\(self$.pointee)'!") + } + let cache$ = _JNI_MyEnum.myEnumSecondCache + let class$ = cache$.javaClass + let method$ = _JNIMethodIDCache.Method(name: "", signature: "(Ljava/lang/String;)V") + let constructorID$ = cache$[method$] + let newObjectArgs$: [jvalue] = [jvalue(l: _0.getJNIValue(in: environment) ?? nil)] + return environment.interface.NewObjectA(environment, class$, constructorID$, newObjectArgs$) + } + """, + """ + @_cdecl("Java_com_example_swift_MyEnum__00024getAsThird__J") + func Java_com_example_swift_MyEnum__00024getAsThird__J(environment: UnsafeMutablePointer!, thisClass: jclass, self: jlong) -> jobject? { + ... + guard case .third(let x, let y) = self$.pointee else { + fatalError("Expected enum case 'third', but was '\\(self$.pointee)'!") + } + let cache$ = _JNI_MyEnum.myEnumThirdCache + let class$ = cache$.javaClass + let method$ = _JNIMethodIDCache.Method(name: "", signature: "(JI)V") + let constructorID$ = cache$[method$] + let newObjectArgs$: [jvalue] = [jvalue(j: x.getJNIValue(in: environment)), jvalue(i: y.getJNIValue(in: environment))] + return environment.interface.NewObjectA(environment, class$, constructorID$, newObjectArgs$) + } + """ + ]) + } +} diff --git a/Tests/JExtractSwiftTests/JNI/JNIExtensionTests.swift b/Tests/JExtractSwiftTests/JNI/JNIExtensionTests.swift new file mode 100644 index 000000000..e6cb10605 --- /dev/null +++ b/Tests/JExtractSwiftTests/JNI/JNIExtensionTests.swift @@ -0,0 +1,65 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JExtractSwiftLib +import Testing + +@Suite +struct JNIExtensionTests { + let interfaceFile = + """ + extension MyStruct { + public var variableInExtension: String { get } + public func methodInExtension() {} + } + + public protocol MyProtocol {} + public struct MyStruct {} + extension MyStruct: MyProtocol {} + """ + + @Test("Import extensions: Java methods") + func import_javaMethods() throws { + try assertOutput( + input: interfaceFile, + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public final class MyStruct implements JNISwiftInstance, MyProtocol { + ... + public void methodInExtension() { + ... + } + """ + ]) + } + + @Test("Import extensions: Computed variables") + func import_computedVariables() throws { + try assertOutput( + input: interfaceFile, + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public final class MyStruct implements JNISwiftInstance, MyProtocol { + ... + public java.lang.String getVariableInExtension() { + ... + } + """ + ]) + } +} diff --git a/Tests/JExtractSwiftTests/JNI/JNIJavaKitTests.swift b/Tests/JExtractSwiftTests/JNI/JNIJavaKitTests.swift new file mode 100644 index 000000000..69d77b737 --- /dev/null +++ b/Tests/JExtractSwiftTests/JNI/JNIJavaKitTests.swift @@ -0,0 +1,80 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JExtractSwiftLib +import Testing + +@Suite +struct JNIJavaKitTests { + let source = + """ + public func function(javaLong: JavaLong, javaInteger: JavaInteger, int: Int64) {} + """ + + let classLookupTable = [ + "JavaLong": "java.lang.Long", + "JavaInteger": "java.lang.Integer" + ] + + @Test + func function_javaBindings() throws { + try assertOutput( + input: source, + .jni, + .java, + javaClassLookupTable: classLookupTable, + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func function(javaLong: JavaLong, javaInteger: JavaInteger, int: Int64) + * } + */ + public static void function(java.lang.Long javaLong, java.lang.Integer javaInteger, long int) { + SwiftModule.$function(javaLong, javaInteger, int); + } + """, + """ + private static native void $function(java.lang.Long javaLong, java.lang.Integer javaInteger, long int); + """ + ] + ) + } + + @Test + func function_swiftThunks() throws { + try assertOutput( + input: source, + .jni, + .swift, + detectChunkByInitialLines: 1, + javaClassLookupTable: classLookupTable, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024function__Ljava_lang_Long_2Ljava_lang_Integer_2J") + func Java_com_example_swift_SwiftModule__00024function__Ljava_lang_Long_2Ljava_lang_Integer_2J(environment: UnsafeMutablePointer!, thisClass: jclass, javaLong: jobject?, javaInteger: jobject?, int: jlong) { + guard let javaLong_unwrapped$ = javaLong else { + fatalError("javaLong was null in call to \\(#function), but Swift requires non-optional!") + } + guard let javaInteger_unwrapped$ = javaInteger else { + fatalError("javaInteger was null in call to \\(#function), but Swift requires non-optional!") + } + SwiftModule.function(javaLong: JavaLong(javaThis: javaLong_unwrapped$, environment: environment), javaInteger: JavaInteger(javaThis: javaInteger_unwrapped$, environment: environment), int: Int64(fromJNI: int, in: environment)) + } + """ + ] + ) + } +} diff --git a/Tests/JExtractSwiftTests/JNI/JNIModuleTests.swift b/Tests/JExtractSwiftTests/JNI/JNIModuleTests.swift new file mode 100644 index 000000000..dddf11478 --- /dev/null +++ b/Tests/JExtractSwiftTests/JNI/JNIModuleTests.swift @@ -0,0 +1,260 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JExtractSwiftLib +import Testing + +@Suite +struct JNIModuleTests { + let globalMethodWithPrimitives = """ + public func helloWorld() + public func takeIntegers(i1: Int8, i2: Int16, i3: Int32, i4: Int64) -> UInt16 + public func otherPrimitives(b: Bool, f: Float, d: Double) + """ + + let globalMethodWithString = """ + public func copy(_ string: String) -> String + """ + + let globalMethodThrowing = """ + public func methodA() throws + public func methodB() throws -> Int64 + """ + + @Test + func generatesModuleJavaClass() throws { + let input = "public func helloWorld()" + + try assertOutput(input: input, .jni, .java, expectedChunks: [ + """ + // Generated by jextract-swift + // Swift module: SwiftModule + + package com.example.swift; + + import org.swift.swiftkit.core.*; + import org.swift.swiftkit.core.util.*; + import java.util.*; + import java.util.concurrent.atomic.AtomicBoolean; + import org.swift.swiftkit.core.annotations.*; + + public final class SwiftModule { + static final String LIB_NAME = "SwiftModule"; + + static { + System.loadLibrary(LIB_NAME); + } + """ + ]) + } + + @Test + func globalMethodWithPrimitives_javaBindings() throws { + try assertOutput( + input: globalMethodWithPrimitives, + .jni, + .java, + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func helloWorld() + * } + */ + public static void helloWorld() { + SwiftModule.$helloWorld(); + } + """, + """ + private static native void $helloWorld(); + """, + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func takeIntegers(i1: Int8, i2: Int16, i3: Int32, i4: Int64) -> UInt16 + * } + */ + @Unsigned + public static char takeIntegers(byte i1, short i2, int i3, long i4) { + return SwiftModule.$takeIntegers(i1, i2, i3, i4); + } + """, + """ + private static native char $takeIntegers(byte i1, short i2, int i3, long i4); + """, + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func otherPrimitives(b: Bool, f: Float, d: Double) + * } + */ + public static void otherPrimitives(boolean b, float f, double d) { + SwiftModule.$otherPrimitives(b, f, d); + } + """, + """ + private static native void $otherPrimitives(boolean b, float f, double d); + """ + ] + ) + } + + @Test + func globalMethodWithPrimitives_swiftThunks() throws { + try assertOutput( + input: globalMethodWithPrimitives, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024helloWorld__") + func Java_com_example_swift_SwiftModule__00024helloWorld__(environment: UnsafeMutablePointer!, thisClass: jclass) { + SwiftModule.helloWorld() + } + """, + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024takeIntegers__BSIJ") + func Java_com_example_swift_SwiftModule__00024takeIntegers__BSIJ(environment: UnsafeMutablePointer!, thisClass: jclass, i1: jbyte, i2: jshort, i3: jint, i4: jlong) -> jchar { + return SwiftModule.takeIntegers(i1: Int8(fromJNI: i1, in: environment), i2: Int16(fromJNI: i2, in: environment), i3: Int32(fromJNI: i3, in: environment), i4: Int64(fromJNI: i4, in: environment)).getJNIValue(in: environment) + } + """, + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024otherPrimitives__ZFD") + func Java_com_example_swift_SwiftModule__00024otherPrimitives__ZFD(environment: UnsafeMutablePointer!, thisClass: jclass, b: jboolean, f: jfloat, d: jdouble) { + SwiftModule.otherPrimitives(b: Bool(fromJNI: b, in: environment), f: Float(fromJNI: f, in: environment), d: Double(fromJNI: d, in: environment)) + } + """ + ] + ) + } + + @Test + func globalMethodWithString_javaBindings() throws { + try assertOutput( + input: globalMethodWithString, + .jni, + .java, + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func copy(_ string: String) -> String + * } + */ + public static java.lang.String copy(java.lang.String string) { + return SwiftModule.$copy(string); + } + """, + """ + private static native java.lang.String $copy(java.lang.String string); + """ + ] + ) + } + + @Test + func globalMethodWithString_swiftThunks() throws { + try assertOutput( + input: globalMethodWithString, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024copy__Ljava_lang_String_2") + func Java_com_example_swift_SwiftModule__00024copy__Ljava_lang_String_2(environment: UnsafeMutablePointer!, thisClass: jclass, string: jstring?) -> jstring? { + return SwiftModule.copy(String(fromJNI: string, in: environment)).getJNIValue(in: environment) + } + """, + ] + ) + } + + @Test + func globalMethodThrowing_javaBindings() throws { + try assertOutput( + input: globalMethodThrowing, + .jni, + .java, + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func methodA() throws + * } + */ + public static void methodA() throws Exception { + SwiftModule.$methodA(); + } + """, + """ + private static native void $methodA(); + """, + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func methodB() throws -> Int64 + * } + */ + public static long methodB() throws Exception { + return SwiftModule.$methodB(); + } + """, + """ + private static native long $methodB(); + """ + ] + ) + } + + @Test + func globalMethodThrowing_swiftThunks() throws { + try assertOutput( + input: globalMethodThrowing, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024methodA__") + func Java_com_example_swift_SwiftModule__00024methodA__(environment: UnsafeMutablePointer!, thisClass: jclass) { + do { + try SwiftModule.methodA() + } catch { + environment.throwAsException(error) + } + } + """, + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024methodB__") + func Java_com_example_swift_SwiftModule__00024methodB__(environment: UnsafeMutablePointer!, thisClass: jclass) -> jlong { + do { + return try SwiftModule.methodB().getJNIValue(in: environment) + } catch { + environment.throwAsException(error) + return Int64.jniPlaceholderValue + } + } + """, + ] + ) + } +} diff --git a/Tests/JExtractSwiftTests/JNI/JNINestedTypesTests.swift b/Tests/JExtractSwiftTests/JNI/JNINestedTypesTests.swift new file mode 100644 index 000000000..67d966e3a --- /dev/null +++ b/Tests/JExtractSwiftTests/JNI/JNINestedTypesTests.swift @@ -0,0 +1,137 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JExtractSwiftLib +import Testing + +@Suite +struct JNINestedTypesTests { + let source1 = """ + public class A { + public class B { + public func g(c: C) {} + + public struct C { + public func h(b: B) {} + } + } + } + + public func f(a: A, b: A.B, c: A.B.C) {} + """ + + @Test("Import: class and struct A.B.C (Java)") + func nestedClassesAndStructs_java() throws { + try assertOutput( + input: source1, + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public final class A implements JNISwiftInstance { + ... + public static final class B implements JNISwiftInstance { + ... + public static final class C implements JNISwiftInstance { + ... + public void h(A.B b) { + ... + } + ... + public void g(A.B.C c) { + ... + } + ... + } + """, + """ + public static void f(A a, A.B b, A.B.C c) { + ... + } + ... + """ + ] + ) + } + + @Test("Import: class and struct A.B.C (Swift)") + func nestedClassesAndStructs_swift() throws { + try assertOutput( + input: source1, + .jni, .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_A__00024destroy__J") + func Java_com_example_swift_A__00024destroy__J(environment: UnsafeMutablePointer!, thisClass: jclass, selfPointer: jlong) { + ... + } + """, + """ + @_cdecl("Java_com_example_swift_A_00024B__00024destroy__J") + func Java_com_example_swift_A_00024B__00024destroy__J(environment: UnsafeMutablePointer!, thisClass: jclass, selfPointer: jlong) { + ... + } + """, + """ + @_cdecl("Java_com_example_swift_A_00024B__00024destroy__J") + func Java_com_example_swift_A_00024B__00024destroy__J(environment: UnsafeMutablePointer!, thisClass: jclass, selfPointer: jlong) { + ... + } + """, + """ + @_cdecl("Java_com_example_swift_A_00024B_00024C__00024h__JJ") + func Java_com_example_swift_A_00024B_00024C__00024h__JJ(environment: UnsafeMutablePointer!, thisClass: jclass, b: jlong, self: jlong) { + ... + } + """ + ] + ) + } + + @Test("Import: nested in enum") + func nestedEnums_java() throws { + try assertOutput( + input: """ + public enum MyError { + case text(TextMessage) + + public struct TextMessage {} + } + + public func f(text: MyError.TextMessage) {} + """, + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public final class MyError implements JNISwiftInstance { + ... + public static final class TextMessage implements JNISwiftInstance { + ... + } + ... + public static MyError text(MyError.TextMessage arg0, SwiftArena swiftArena$) { + ... + } + """, + """ + public static void f(MyError.TextMessage text) { + ... + } + """ + ] + ) + } +} \ No newline at end of file diff --git a/Tests/JExtractSwiftTests/JNI/JNIOptionalTests.swift b/Tests/JExtractSwiftTests/JNI/JNIOptionalTests.swift new file mode 100644 index 000000000..6c931d7b4 --- /dev/null +++ b/Tests/JExtractSwiftTests/JNI/JNIOptionalTests.swift @@ -0,0 +1,253 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JExtractSwiftLib +import Testing + +@Suite +struct JNIOptionalTests { + let source = + """ + class MyClass { } + + public func optionalSugar(_ arg: Int64?) -> Int32? + public func optionalExplicit(_ arg: Optional) -> Optional + public func optionalClass(_ arg: MyClass?) -> MyClass? + public func optionalJavaKitClass(_ arg: JavaLong?) + """ + + let classLookupTable = [ + "JavaLong": "java.lang.Long", + ] + + @Test + func optionalSugar_javaBindings() throws { + try assertOutput( + input: source, + .jni, + .java, + javaClassLookupTable: classLookupTable, + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func optionalSugar(_ arg: Int64?) -> Int32? + * } + */ + public static OptionalInt optionalSugar(OptionalLong arg) { + long result_combined$ = SwiftModule.$optionalSugar((byte) (arg.isPresent() ? 1 : 0), arg.orElse(0L)); + byte result_discriminator$ = (byte) (result_combined$ & 0xFF); + int result_value$ = (int) (result_combined$ >> 32); + return result_discriminator$ == 1 ? OptionalInt.of(result_value$) : OptionalInt.empty(); + } + """, + """ + private static native long $optionalSugar(byte arg_discriminator, long arg_value); + """ + ] + ) + } + + @Test + func optionalSugar_swiftThunks() throws { + try assertOutput( + input: source, + .jni, + .swift, + detectChunkByInitialLines: 1, + javaClassLookupTable: classLookupTable, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024optionalSugar__BJ") + func Java_com_example_swift_SwiftModule__00024optionalSugar__BJ(environment: UnsafeMutablePointer!, thisClass: jclass, arg_discriminator: jbyte, arg_value: jlong) -> jlong { + let result_value$ = SwiftModule.optionalSugar(arg_discriminator == 1 ? Int64(fromJNI: arg_value, in: environment) : nil).map { + Int64($0) << 32 | Int64(1) + } ?? 0 + return result_value$.getJNIValue(in: environment) + } + """ + ] + ) + } + + @Test + func optionalExplicit_javaBindings() throws { + try assertOutput( + input: source, + .jni, + .java, + javaClassLookupTable: classLookupTable, + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func optionalExplicit(_ arg: Optional) -> Optional + * } + */ + public static Optional optionalExplicit(Optional arg) { + byte[] result$_discriminator$ = new byte[1]; + java.lang.String result$ = SwiftModule.$optionalExplicit((byte) (arg.isPresent() ? 1 : 0), arg.orElse(null), result$_discriminator$); + return (result$_discriminator$[0] == 1) ? Optional.of(result$) : Optional.empty(); + } + """, + """ + private static native java.lang.String $optionalExplicit(byte arg_discriminator, java.lang.String arg_value, byte[] result_discriminator$); + """ + ] + ) + } + + @Test + func optionalExplicit_swiftThunks() throws { + try assertOutput( + input: source, + .jni, + .swift, + detectChunkByInitialLines: 1, + javaClassLookupTable: classLookupTable, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024optionalExplicit__BLjava_lang_String_2_3B") + func Java_com_example_swift_SwiftModule__00024optionalExplicit__BLjava_lang_String_2_3B(environment: UnsafeMutablePointer!, thisClass: jclass, arg_discriminator: jbyte, arg_value: jstring?, result_discriminator$: jbyteArray?) -> jstring? { + let result$: jstring? + if let innerResult$ = SwiftModule.optionalExplicit(arg_discriminator == 1 ? String(fromJNI: arg_value, in: environment) : nil) { + result$ = innerResult$.getJNIValue(in: environment) + var flag$ = Int8(1) + environment.interface.SetByteArrayRegion(environment, result_discriminator$, 0, 1, &flag$) + } + else { + result$ = String.jniPlaceholderValue + var flag$ = Int8(0) + environment.interface.SetByteArrayRegion(environment, result_discriminator$, 0, 1, &flag$) + } + return result$ + } + """ + ] + ) + } + + @Test + func optionalClass_javaBindings() throws { + try assertOutput( + input: source, + .jni, + .java, + javaClassLookupTable: classLookupTable, + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func optionalClass(_ arg: MyClass?) -> MyClass? + * } + */ + public static Optional optionalClass(Optional arg, SwiftArena swiftArena$) { + byte[] result$_discriminator$ = new byte[1]; + long result$ = SwiftModule.$optionalClass(arg.map(MyClass::$memoryAddress).orElse(0L), result$_discriminator$); + return (result$_discriminator$[0] == 1) ? Optional.of(MyClass.wrapMemoryAddressUnsafe(result$, swiftArena$)) : Optional.empty(); + } + """, + """ + private static native long $optionalClass(long arg, byte[] result_discriminator$); + """ + ] + ) + } + + @Test + func optionalClass_swiftThunks() throws { + try assertOutput( + input: source, + .jni, + .swift, + detectChunkByInitialLines: 1, + javaClassLookupTable: classLookupTable, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024optionalClass__J_3B") + func Java_com_example_swift_SwiftModule__00024optionalClass__J_3B(environment: UnsafeMutablePointer!, thisClass: jclass, arg: jlong, result_discriminator$: jbyteArray?) -> jlong { + let argBits$ = Int(Int64(fromJNI: arg, in: environment)) + let arg$ = UnsafeMutablePointer(bitPattern: argBits$) + let result$: jlong + if let innerResult$ = SwiftModule.optionalClass(arg$?.pointee) { + let _result$ = UnsafeMutablePointer.allocate(capacity: 1) + _result$.initialize(to: innerResult$) + let _resultBits$ = Int64(Int(bitPattern: _result$)) + result$ = _resultBits$.getJNIValue(in: environment) + var flag$ = Int8(1) + environment.interface.SetByteArrayRegion(environment, result_discriminator$, 0, 1, &flag$) + } + else { + result$ = 0 + var flag$ = Int8(0) + environment.interface.SetByteArrayRegion(environment, result_discriminator$, 0, 1, &flag$) + } + return result$ + } + """ + ] + ) + } + + @Test + func optionalJavaKitClass_javaBindings() throws { + try assertOutput( + input: source, + .jni, + .java, + javaClassLookupTable: classLookupTable, + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func optionalJavaKitClass(_ arg: JavaLong?) + * } + */ + public static void optionalJavaKitClass(Optional arg) { + SwiftModule.$optionalJavaKitClass(arg.orElse(null)); + } + """, + """ + private static native void $optionalJavaKitClass(java.lang.Long arg); + """ + ] + ) + } + + @Test + func optionalJavaKitClass_swiftThunks() throws { + try assertOutput( + input: source, + .jni, + .swift, + detectChunkByInitialLines: 1, + javaClassLookupTable: classLookupTable, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024optionalJavaKitClass__Ljava_lang_Long_2") + func Java_com_example_swift_SwiftModule__00024optionalJavaKitClass__Ljava_lang_Long_2(environment: UnsafeMutablePointer!, thisClass: jclass, arg: jobject?) { + SwiftModule.optionalJavaKitClass(arg.map { + return JavaLong(javaThis: $0, environment: environment) + } + ) + } + """ + ] + ) + } +} diff --git a/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift b/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift new file mode 100644 index 000000000..3b47fd100 --- /dev/null +++ b/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift @@ -0,0 +1,353 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftJavaConfigurationShared +import JExtractSwiftLib +import Testing + +@Suite +struct JNIProtocolTests { + var config: Configuration { + var config = Configuration() + config.enableJavaCallbacks = true + return config + } + + let source = """ + public protocol SomeProtocol { + public func method() {} + public func withObject(c: SomeClass) -> SomeClass {} + } + + public protocol B {} + + public class SomeClass: SomeProtocol { + public func makeClass() -> SomeClass {} + } + + public func takeProtocol(x: some SomeProtocol, y: any SomeProtocol) + public func takeGeneric(s: S) + public func takeComposite(x: any SomeProtocol & B) + """ + + @Test + func generatesJavaInterface() throws { + try assertOutput( + input: source, + config: config, + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + // Generated by jextract-swift + // Swift module: SwiftModule + + package com.example.swift; + + import org.swift.swiftkit.core.*; + import org.swift.swiftkit.core.util.*; + """, + """ + public interface SomeProtocol { + ... + public void method(); + ... + public SomeClass withObject(SomeClass c, SwiftArena swiftArena$); + ... + } + """ + ]) + } + + @Test + func emitsDefault() throws { + try assertOutput( + input: source, + config: config, + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public interface SomeProtocol { + ... + public default SomeClass withObject(SomeClass c) { + return withObject(c, SwiftMemoryManagement.GLOBAL_SWIFT_JAVA_ARENA); + } + ... + public SomeClass withObject(SomeClass c, SwiftArena swiftArena$); + ... + } + """ + ]) + } + + @Test + func generatesJavaClassWithExtends() throws { + try assertOutput( + input: source, + config: config, + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public final class SomeClass implements JNISwiftInstance, SomeProtocol { + ... + public SomeClass makeClass(SwiftArena swiftArena$) { + ... + } + """, + ]) + } + + @Test + func takeProtocol_java() throws { + try assertOutput( + input: source, + config: config, + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public static <_T0 extends SomeProtocol, _T1 extends SomeProtocol> void takeProtocol(_T0 x, _T1 y) { + SwiftModule.$takeProtocol(x, y); + } + """, + """ + private static native void $takeProtocol(java.lang.Object x, java.lang.Object y); + """ + ]) + } + + @Test + func takeProtocol_swift() throws { + try assertOutput( + input: source, + config: config, + .jni, .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + final class _SwiftModule_takeGeneric_s_Wrapper: SwiftJavaSomeProtocolWrapper { + let _javaSomeProtocolInterface: JavaSomeProtocol + init(_javaSomeProtocolInterface: JavaSomeProtocol) { + self._javaSomeProtocolInterface = _javaSomeProtocolInterface + } + } + """, + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024takeProtocol__Ljava_lang_Object_2Ljava_lang_Object_2") + func Java_com_example_swift_SwiftModule__00024takeProtocol__Ljava_lang_Object_2Ljava_lang_Object_2(environment: UnsafeMutablePointer!, thisClass: jclass, x: jobject?, y: jobject?) { + let xswiftObject$: (SomeProtocol) + if environment.interface.IsInstanceOf(environment, x, _JNIMethodIDCache.JNISwiftInstance.class) != 0 { + ... + let xpointer$DynamicType$: Any.Type = unsafeBitCast(xpointer$TypeMetadataPointer$, to: Any.Type.self) + guard let xpointer$RawPointer$ = UnsafeMutableRawPointer(bitPattern: Int(Int64(fromJNI: xpointer$, in: environment))) else { + fatalError("xpointer$ memory address was null") + } + #if hasFeature(ImplicitOpenExistentials) + let xpointer$Existential$ = xpointer$RawPointer$.load(as: xpointer$DynamicType$) as! any (SomeProtocol) + #else + func xpointer$DoLoad(_ ty: Ty.Type) -> any (SomeProtocol) { + xpointer$RawPointer$.load(as: ty) as! any (SomeProtocol) + } + let xpointer$Existential$ = _openExistential(xpointer$DynamicType$, do: xpointer$DoLoad) + #endif + xswiftObject$ = xpointer$Existential$ + } + else { + xswiftObject$ = _SwiftModule_takeProtocol_x_Wrapper(_javaSomeProtocolInterface: JavaSomeProtocol(javaThis: x!, environment: environment)) + } + let yswiftObject$: (SomeProtocol) + if environment.interface.IsInstanceOf(environment, y, _JNIMethodIDCache.JNISwiftInstance.class) != 0 { + ... + yswiftObject$ = ypointer$Existential$ + } + else { + yswiftObject$ = _SwiftModule_takeProtocol_y_Wrapper(_javaSomeProtocolInterface: JavaSomeProtocol(javaThis: y!, environment: environment)) + } + SwiftModule.takeProtocol(x: xswiftObject$, y: yswiftObject$) + } + """ + ] + ) + } + + @Test + func takeGeneric_java() throws { + try assertOutput( + input: source, + config: config, + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public static void takeGeneric(S s) { + SwiftModule.$takeGeneric(s); + } + """, + """ + private static native void $takeGeneric(java.lang.Object s); + """ + ]) + } + + @Test + func takeGeneric_swift() throws { + try assertOutput( + input: source, + config: config, + .jni, .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + final class _SwiftModule_takeGeneric_s_Wrapper: SwiftJavaSomeProtocolWrapper { + let _javaSomeProtocolInterface: JavaSomeProtocol + init(_javaSomeProtocolInterface: JavaSomeProtocol) { + self._javaSomeProtocolInterface = _javaSomeProtocolInterface + } + } + """, + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024takeGeneric__Ljava_lang_Object_2") + func Java_com_example_swift_SwiftModule__00024takeGeneric__Ljava_lang_Object_2(environment: UnsafeMutablePointer!, thisClass: jclass, s: jobject?) { + let sswiftObject$: (SomeProtocol) + if environment.interface.IsInstanceOf(environment, s, _JNIMethodIDCache.JNISwiftInstance.class) != 0 { + ... + sswiftObject$ = spointer$Existential$ + } + else { + sswiftObject$ = _SwiftModule_takeGeneric_s_Wrapper(_javaSomeProtocolInterface: JavaSomeProtocol(javaThis: s!, environment: environment)) + } + SwiftModule.takeGeneric(s: sswiftObject$) + } + """ + ] + ) + } + + @Test + func takeComposite_java() throws { + try assertOutput( + input: source, + config: config, + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public static <_T0 extends SomeProtocol & B> void takeComposite(_T0 x) { + SwiftModule.$takeComposite(x); + } + """, + """ + private static native void $takeComposite(java.lang.Object x); + """ + ]) + } + + @Test + func takeComposite_swift() throws { + try assertOutput( + input: source, + config: config, + .jni, .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + final class _SwiftModule_takeComposite_x_Wrapper: SwiftJavaSomeProtocolWrapper, SwiftJavaBWrapper { + let _javaSomeProtocolInterface: JavaSomeProtocol + let _javaBInterface: JavaB + init(_javaSomeProtocolInterface: JavaSomeProtocol, _javaBInterface: JavaB) { + self._javaSomeProtocolInterface = _javaSomeProtocolInterface + self._javaBInterface = _javaBInterface + } + } + """, + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024takeComposite__Ljava_lang_Object_2") + func Java_com_example_swift_SwiftModule__00024takeComposite__Ljava_lang_Object_2(environment: UnsafeMutablePointer!, thisClass: jclass, x: jobject?) { + let xswiftObject$: (SomeProtocol & B) + if environment.interface.IsInstanceOf(environment, x, _JNIMethodIDCache.JNISwiftInstance.class) != 0 { + let xpointer$ = environment.interface.CallLongMethodA(environment, x, _JNIMethodIDCache.JNISwiftInstance.memoryAddress, []) + let xtypeMetadata$ = environment.interface.CallLongMethodA(environment, x, _JNIMethodIDCache.JNISwiftInstance.typeMetadataAddress, []) + guard let xpointer$TypeMetadataPointer$ = UnsafeRawPointer(bitPattern: Int(Int64(fromJNI: xtypeMetadata$, in: environment))) else { + fatalError("xtypeMetadata$ memory address was null") + } + let xpointer$DynamicType$: Any.Type = unsafeBitCast(xpointer$TypeMetadataPointer$, to: Any.Type.self) + guard let xpointer$RawPointer$ = UnsafeMutableRawPointer(bitPattern: Int(Int64(fromJNI: xpointer$, in: environment))) else { + fatalError("xpointer$ memory address was null") + } + #if hasFeature(ImplicitOpenExistentials) + let xpointer$Existential$ = xpointer$RawPointer$.load(as: xpointer$DynamicType$) as! any (SomeProtocol & B) + #else + func xpointer$DoLoad(_ ty: Ty.Type) -> any (SomeProtocol & B) { + xpointer$RawPointer$.load(as: ty) as! any (SomeProtocol & B) + } + let xpointer$Existential$ = _openExistential(xpointer$DynamicType$, do: xpointer$DoLoad) + #endif + xswiftObject$ = xpointer$Existential$ + } + else { + xswiftObject$ = _SwiftModule_takeComposite_x_Wrapper(_javaSomeProtocolInterface: JavaSomeProtocol(javaThis: x!, environment: environment), _javaBInterface: JavaB(javaThis: x!, environment: environment)) + } + SwiftModule.takeComposite(x: xswiftObject$) + } + """ + ] + ) + } + + @Test + func generatesProtocolWrappers() throws { + try assertOutput( + input: source, + config: config, + .jni, .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + protocol SwiftJavaSomeProtocolWrapper: SomeProtocol { + var _javaSomeProtocolInterface: JavaSomeProtocol { get } + } + """, + """ + extension SwiftJavaSomeProtocolWrapper { + public func method() { + _javaSomeProtocolInterface.method() + } + public func withObject(c: SomeClass) -> SomeClass { + let cClass = try! JavaClass(environment: JavaVirtualMachine.shared().environment()) + let cPointer = UnsafeMutablePointer.allocate(capacity: 1) + cPointer.initialize(to: c) + guard let unwrapped$ = _javaSomeProtocolInterface.withObject(cClass.wrapMemoryAddressUnsafe(Int64(Int(bitPattern: cPointer)))) else { + fatalError("Upcall to withObject unexpectedly returned nil") + } + let result$MemoryAddress$ = unwrapped$.as(JavaJNISwiftInstance.self)!.memoryAddress() + let result$Pointer = UnsafeMutablePointer(bitPattern: Int(result$MemoryAddress$))! + return result$Pointer.pointee + } + } + """, + """ + protocol SwiftJavaBWrapper: B { + var _javaBInterface: JavaB { get } + } + """, + """ + extension SwiftJavaBWrapper { + } + """ + ] + ) + } +} diff --git a/Tests/JExtractSwiftTests/JNI/JNIStructTests.swift b/Tests/JExtractSwiftTests/JNI/JNIStructTests.swift new file mode 100644 index 000000000..f8830b644 --- /dev/null +++ b/Tests/JExtractSwiftTests/JNI/JNIStructTests.swift @@ -0,0 +1,228 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JExtractSwiftLib +import Testing + +@Suite +struct JNIStructTests { + let source = """ + public struct MyStruct { + let x: Int64 + let y: Int64 + + public init(x: Int64, y: Int64) { + self.x = y + self.y = y + } + + public func doSomething(x: Int64) {} + } + """ + + @Test + func generatesJavaClass() throws { + try assertOutput( + input: source, .jni, .java, + expectedChunks: [ + """ + // Generated by jextract-swift + // Swift module: SwiftModule + + package com.example.swift; + + import org.swift.swiftkit.core.*; + import org.swift.swiftkit.core.util.*; + """,]) + try assertOutput(input: source, .jni, .java, expectedChunks: [ + """ + public final class MyStruct implements JNISwiftInstance { + static final String LIB_NAME = "SwiftModule"; + + @SuppressWarnings("unused") + private static final boolean INITIALIZED_LIBS = initializeLibs(); + static boolean initializeLibs() { + System.loadLibrary(LIB_NAME); + return true; + } + """, + """ + private MyStruct(long selfPointer, SwiftArena swiftArena) { + SwiftObjects.requireNonZero(selfPointer, "selfPointer"); + this.selfPointer = selfPointer; + + // Only register once we have fully initialized the object since this will need the object pointer. + swiftArena.register(this); + } + """, + """ + public static MyStruct wrapMemoryAddressUnsafe(long selfPointer, SwiftArena swiftArena) { + return new MyStruct(selfPointer, swiftArena); + } + """ + ]) + try assertOutput( + input: source, .jni, .java, + expectedChunks: [ + """ + private static native void $destroy(long selfPointer); + """ + ]) + try assertOutput( + input: source, .jni, .java, + expectedChunks: [ + """ + @Override + public Runnable $createDestroyFunction() { + long self$ = this.$memoryAddress(); + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall("MyStruct.$createDestroyFunction", + "this", this, + "self", self$); + } + return new Runnable() { + @Override + public void run() { + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall("MyStruct.$destroy", "self", self$); + } + MyStruct.$destroy(self$); + } + }; + } + """ + ]) + } + + @Test + func initializer_javaBindings() throws { + try assertOutput( + input: source, + .jni, + .java, + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public init(x: Int64, y: Int64) + * } + */ + public static MyStruct init(long x, long y, SwiftArena swiftArena$) { + return MyStruct.wrapMemoryAddressUnsafe(MyStruct.$init(x, y), swiftArena$); + } + """, + """ + private static native long $init(long x, long y); + """, + ] + ) + } + + @Test + func initializer_swiftThunks() throws { + try assertOutput( + input: source, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_MyStruct__00024init__JJ") + func Java_com_example_swift_MyStruct__00024init__JJ(environment: UnsafeMutablePointer!, thisClass: jclass, x: jlong, y: jlong) -> jlong { + let result$ = UnsafeMutablePointer.allocate(capacity: 1) + result$.initialize(to: MyStruct.init(x: Int64(fromJNI: x, in: environment), y: Int64(fromJNI: y, in: environment))) + let resultBits$ = Int64(Int(bitPattern: result$)) + return resultBits$.getJNIValue(in: environment) + } + """ + ] + ) + } + + @Test + func destroyFunction_swiftThunks() throws { + try assertOutput( + input: source, + .jni, + .swift, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_MyStruct__00024destroy__J") + func Java_com_example_swift_MyStruct__00024destroy__J(environment: UnsafeMutablePointer!, thisClass: jclass, selfPointer: jlong) { + guard let env$ = environment else { + fatalError("Missing JNIEnv in downcall to \\(#function)") + } + assert(selfPointer != 0, "selfPointer memory address was null") + let selfBits$ = Int(Int64(fromJNI: selfPointer, in: env$)) + guard let self$ = UnsafeMutablePointer(bitPattern: selfBits$) else { + fatalError("self memory address was null in call to \\(#function)!") + } + self$.deinitialize(count: 1) + self$.deallocate() + } + """ + ] + ) + } + + @Test + func memberMethod_javaBindings() throws { + try assertOutput( + input: source, + .jni, + .java, + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func doSomething(x: Int64) + * } + */ + public void doSomething(long x) { + MyStruct.$doSomething(x, this.$memoryAddress()); + } + """, + """ + private static native void $doSomething(long x, long self); + """, + ] + ) + } + + @Test + func memberMethod_swiftThunks() throws { + try assertOutput( + input: source, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_MyStruct__00024doSomething__JJ") + func Java_com_example_swift_MyStruct__00024doSomething__JJ(environment: UnsafeMutablePointer!, thisClass: jclass, x: jlong, self: jlong) { + assert(self != 0, "self memory address was null") + let selfBits$ = Int(Int64(fromJNI: self, in: environment)) + let self$ = UnsafeMutablePointer(bitPattern: selfBits$) + guard let self$ else { + fatalError("self memory address was null in call to \\(#function)!") + } + self$.pointee.doSomething(x: Int64(fromJNI: x, in: environment)) + } + """ + ] + ) + } +} diff --git a/Tests/JExtractSwiftTests/JNI/JNISubscriptsTests.swift b/Tests/JExtractSwiftTests/JNI/JNISubscriptsTests.swift new file mode 100644 index 000000000..0f7b131d0 --- /dev/null +++ b/Tests/JExtractSwiftTests/JNI/JNISubscriptsTests.swift @@ -0,0 +1,155 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JExtractSwiftLib +import Testing + +@Suite +struct JNISubscriptsTests { + private let noParamsSubscriptSource = """ + public struct MyStruct { + private var testVariable: Double = 0 + + public subscript() -> Double { + get { return testVariable } + set { testVariable = newValue } + } + } + """ + + private let subscriptWithParamsSource = """ + public struct MyStruct { + private var testVariable: [Int32] = [] + + public subscript(index: Int32) -> Int32 { + get { return testVariable[Int(index)] } + set { testVariable[Int(index)] = newValue } + } + } + """ + + @Test("Test generation of JavaClass for subscript with no parameters") + func generatesJavaClassForNoParams() throws { + try assertOutput(input: noParamsSubscriptSource, .jni, .java, expectedChunks: [ + """ + public double getSubscript() { + return MyStruct.$getSubscript(this.$memoryAddress()); + """, + """ + private static native double $getSubscript(long self); + """, + """ + public void setSubscript(double newValue) { + MyStruct.$setSubscript(newValue, this.$memoryAddress()); + """, + """ + private static native void $setSubscript(double newValue, long self); + """ + ]) + try assertOutput( + input: noParamsSubscriptSource, .jni, .java, + expectedChunks: [ + """ + private static native void $destroy(long selfPointer); + """ + ]) + } + + @Test("Test generation of JavaClass for subscript with parameters") + func generatesJavaClassForParameters() throws { + try assertOutput(input: subscriptWithParamsSource, .jni, .java, expectedChunks: [ + """ + public int getSubscript(int index) { + return MyStruct.$getSubscript(index, this.$memoryAddress()); + + """, + """ + private static native int $getSubscript(int index, long self); + """, + """ + public void setSubscript(int index, int newValue) { + MyStruct.$setSubscript(index, newValue, this.$memoryAddress()); + """, + """ + private static native void $setSubscript(int index, int newValue, long self); + """ + ]) + } + + @Test("Test generation of Swift thunks for subscript without parameters") + func subscriptWithoutParamsMethodSwiftThunk() throws { + try assertOutput( + input: noParamsSubscriptSource, + .jni, + .swift, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_MyStruct__00024getSubscript__J") + func Java_com_example_swift_MyStruct__00024getSubscript__J(environment: UnsafeMutablePointer!, thisClass: jclass, self: jlong) -> jdouble { + assert(self != 0, "self memory address was null") + let selfBits$ = Int(Int64(fromJNI: self, in: environment)) + let self$ = UnsafeMutablePointer(bitPattern: selfBits$) + guard let self$ else { + fatalError("self memory address was null in call to \\(#function)!") + } + return self$.pointee[].getJNIValue(in: environment) + """, + """ + @_cdecl("Java_com_example_swift_MyStruct__00024setSubscript__DJ") + func Java_com_example_swift_MyStruct__00024setSubscript__DJ(environment: UnsafeMutablePointer!, thisClass: jclass, newValue: jdouble, self: jlong) { + assert(self != 0, "self memory address was null") + let selfBits$ = Int(Int64(fromJNI: self, in: environment)) + let self$ = UnsafeMutablePointer(bitPattern: selfBits$) + guard let self$ else { + fatalError("self memory address was null in call to \\(#function)!") + } + self$.pointee[] = Double(fromJNI: newValue, in: environment) + """ + ] + ) + } + + @Test("Test generation of Swift thunks for subscript with parameters") + func subscriptWithParamsMethodSwiftThunk() throws { + try assertOutput( + input: subscriptWithParamsSource, + .jni, + .swift, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_MyStruct__00024getSubscript__IJ") + func Java_com_example_swift_MyStruct__00024getSubscript__IJ(environment: UnsafeMutablePointer!, thisClass: jclass, index: jint, self: jlong) -> jint { + assert(self != 0, "self memory address was null") + let selfBits$ = Int(Int64(fromJNI: self, in: environment)) + let self$ = UnsafeMutablePointer(bitPattern: selfBits$) + guard let self$ else { + fatalError("self memory address was null in call to \\(#function)!") + } + return self$.pointee[Int32(fromJNI: index, in: environment)].getJNIValue(in: environment) + """, + """ + @_cdecl("Java_com_example_swift_MyStruct__00024setSubscript__IIJ") + func Java_com_example_swift_MyStruct__00024setSubscript__IIJ(environment: UnsafeMutablePointer!, thisClass: jclass, index: jint, newValue: jint, self: jlong) { + assert(self != 0, "self memory address was null") + let selfBits$ = Int(Int64(fromJNI: self, in: environment)) + let self$ = UnsafeMutablePointer(bitPattern: selfBits$) + guard let self$ else { + fatalError("self memory address was null in call to \\(#function)!") + } + self$.pointee[Int32(fromJNI: index, in: environment)] = Int32(fromJNI: newValue, in: environment) + """ + ] + ) + } +} diff --git a/Tests/JExtractSwiftTests/JNI/JNIToStringTests.swift b/Tests/JExtractSwiftTests/JNI/JNIToStringTests.swift new file mode 100644 index 000000000..1059002b6 --- /dev/null +++ b/Tests/JExtractSwiftTests/JNI/JNIToStringTests.swift @@ -0,0 +1,96 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JExtractSwiftLib +import Testing +import SwiftJavaConfigurationShared + +@Suite +struct JNIToStringTests { + let source = + """ + public struct MyType {} + """ + + @Test("JNI toString (Java)") + func toString_java() throws { + try assertOutput( + input: source, + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public String toString() { + return $toString(this.$memoryAddress()); + } + """, + """ + private static native java.lang.String $toString(long selfPointer); + """ + ] + ) + } + + @Test("JNI toString (Swift)") + func toString_swift() throws { + try assertOutput( + input: source, + .jni, .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_MyType__00024toString__J") + func Java_com_example_swift_MyType__00024toString__J(environment: UnsafeMutablePointer!, thisClass: jclass, selfPointer: jlong) -> jstring? { + ... + return String(describing: self$.pointee).getJNIValue(in: environment) + } + """, + ] + ) + } + + @Test("JNI toDebugString (Java)") + func toDebugString_java() throws { + try assertOutput( + input: source, + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public String toDebugString() { + return $toDebugString(this.$memoryAddress()); + } + """, + ] + ) + } + + @Test("JNI toDebugString (Swift)") + func toDebugString_swift() throws { + try assertOutput( + input: source, + .jni, .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_MyType__00024toDebugString__J") + func Java_com_example_swift_MyType__00024toDebugString__J(environment: UnsafeMutablePointer!, thisClass: jclass, selfPointer: jlong) -> jstring? { + ... + return String(reflecting: self$.pointee).getJNIValue(in: environment) + } + """, + ] + ) + } +} diff --git a/Tests/JExtractSwiftTests/JNI/JNIUnsignedNumberTests.swift b/Tests/JExtractSwiftTests/JNI/JNIUnsignedNumberTests.swift new file mode 100644 index 000000000..0833b21f3 --- /dev/null +++ b/Tests/JExtractSwiftTests/JNI/JNIUnsignedNumberTests.swift @@ -0,0 +1,153 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JExtractSwiftLib +import SwiftJavaConfigurationShared +import Testing + +final class JNIUnsignedNumberTests { + + @Test("Import: UInt16 (char)") + func jni_unsignedChar() throws { + try assertOutput( + input: "public func unsignedChar(_ arg: UInt16)", + .jni, .java, + detectChunkByInitialLines: 2, + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func unsignedChar(_ arg: UInt16) + * } + */ + public static void unsignedChar(@Unsigned char arg) { + SwiftModule.$unsignedChar(arg); + } + """, + """ + private static native void $unsignedChar(char arg); + """, + ] + ) + } + + @Test("Import: UInt32 (annotate)") + func jni_unsignedInt_annotate() throws { + var config = Configuration() + config.unsignedNumbersMode = .annotate + + try assertOutput( + input: "public func unsignedInt(_ arg: UInt32)", + config: config, + .jni, .java, + detectChunkByInitialLines: 2, + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func unsignedInt(_ arg: UInt32) + * } + */ + public static void unsignedInt(@Unsigned int arg) { + SwiftModule.$unsignedInt(arg); + } + private static native void $unsignedInt(int arg); + """, + ] + ) + } + + @Test("Import: return UInt32 (default)") + func jni_returnUnsignedIntDefault() throws { + let config = Configuration() + + try assertOutput( + input: "public func returnUnsignedInt() -> UInt32", + config: config, + .jni, .java, + detectChunkByInitialLines: 2, + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func returnUnsignedInt() -> UInt32 + * } + */ + @Unsigned + public static int returnUnsignedInt() { + return SwiftModule.$returnUnsignedInt(); + } + private static native int $returnUnsignedInt(); + """, + ] + ) + } + + @Test("Import: return UInt64 (wrap, unsupported)") + func jni_return_unsignedLongWrap() throws { + var config = Configuration() + config.unsignedNumbersMode = .wrapGuava + + try assertOutput( + input: "public func returnUnsignedLong() -> UInt64", + config: config, + .jni, .java, + detectChunkByInitialLines: 2, + expectedChunks: [ + // we do not import in wrap mode + """ + public final class SwiftModule { + static final String LIB_NAME = "SwiftModule"; + + static { + System.loadLibrary(LIB_NAME); + } + + } + """, + ] + ) + } + + @Test("Import: take UInt64 return UInt32 (annotate)") + func jni_echo_unsignedLong_annotate() throws { + var config = Configuration() + config.unsignedNumbersMode = .annotate + + try assertOutput( + input: "public func unsignedLong(first: UInt64, second: UInt32) -> UInt32", + config: config, + .jni, .java, + detectChunkByInitialLines: 2, + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func unsignedLong(first: UInt64, second: UInt32) -> UInt32 + * } + */ + @Unsigned + public static int unsignedLong(@Unsigned long first, @Unsigned int second) { + return SwiftModule.$unsignedLong(first, second); + } + private static native int $unsignedLong(long first, int second); + """, + ] + ) + } +} diff --git a/Tests/JExtractSwiftTests/JNI/JNIVariablesTests.swift b/Tests/JExtractSwiftTests/JNI/JNIVariablesTests.swift new file mode 100644 index 000000000..363c117d6 --- /dev/null +++ b/Tests/JExtractSwiftTests/JNI/JNIVariablesTests.swift @@ -0,0 +1,445 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JExtractSwiftLib +import Testing + +@Suite +struct JNIVariablesTests { + let membersSource = + """ + public class MyClass { + public let someByte: UInt8 + public let constant: Int64 + public var mutable: Int64 + public var computed: Int64 { + return 0 + } + public var computedThrowing: Int64 { + get throws { return 0 } + } + public var getterAndSetter: Int64 { + get { return 0 } + set { } + } + public var someBoolean: Bool + public var isBoolean: Bool + } + """ + + @Test + func constant_javaBindings() throws { + try assertOutput(input: membersSource, .jni, .java, expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public let constant: Int64 + * } + */ + public long getConstant() { + return MyClass.$getConstant(this.$memoryAddress()); + } + """, + """ + private static native long $getConstant(long self); + """ + ]) + } + + @Test + func constant_swiftThunks() throws { + try assertOutput( + input: membersSource, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_MyClass__00024getConstant__J") + func Java_com_example_swift_MyClass__00024getConstant__J(environment: UnsafeMutablePointer!, thisClass: jclass, self: jlong) -> jlong { + ... + return self$.pointee.constant.getJNIValue(in: environment) + } + """ + ] + ) + } + + @Test + func mutable_javaBindings() throws { + try assertOutput( + input: membersSource, + .jni, + .java, + detectChunkByInitialLines: 8, + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public var mutable: Int64 + * } + */ + public long getMutable() { + return MyClass.$getMutable(this.$memoryAddress()); + } + """, + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public var mutable: Int64 + * } + */ + public void setMutable(long newValue) { + MyClass.$setMutable(newValue, this.$memoryAddress()); + } + """, + """ + private static native long $getMutable(long self); + """, + """ + private static native void $setMutable(long newValue, long self); + """ + ] + ) + } + + @Test + func mutable_swiftThunks() throws { + try assertOutput( + input: membersSource, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_MyClass__00024getMutable__J") + func Java_com_example_swift_MyClass__00024getMutable__J(environment: UnsafeMutablePointer!, thisClass: jclass, self: jlong) -> jlong { + assert(self != 0, "self memory address was null") + ... + let self$ = UnsafeMutablePointer(bitPattern: selfBits$) + guard let self$ else { + fatalError("self memory address was null in call to \\(#function)!") + } + ... + return self$.pointee.mutable.getJNIValue(in: environment) + } + """, + """ + @_cdecl("Java_com_example_swift_MyClass__00024setMutable__JJ") + func Java_com_example_swift_MyClass__00024setMutable__JJ(environment: UnsafeMutablePointer!, thisClass: jclass, newValue: jlong, self: jlong) { + assert(self != 0, "self memory address was null") + ... + self$.pointee.mutable = Int64(fromJNI: newValue, in: environment) + } + """ + ] + ) + } + + @Test + func computed_javaBindings() throws { + try assertOutput( + input: membersSource, + .jni, + .java, + detectChunkByInitialLines: 8, + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public var computed: Int64 + * } + */ + public long getComputed() { + return MyClass.$getComputed(this.$memoryAddress()); + } + """, + """ + private static native long $getComputed(long self); + """, + ] + ) + } + + @Test + func computed_swiftThunks() throws { + try assertOutput( + input: membersSource, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_MyClass__00024getComputed__J") + func Java_com_example_swift_MyClass__00024getComputed__J(environment: UnsafeMutablePointer!, thisClass: jclass, self: jlong) -> jlong { + ... + return self$.pointee.computed.getJNIValue(in: environment) + } + """, + ] + ) + } + + @Test + func computedThrowing_javaBindings() throws { + try assertOutput( + input: membersSource, + .jni, + .java, + detectChunkByInitialLines: 8, + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public var computedThrowing: Int64 + * } + */ + public long getComputedThrowing() throws Exception { + return MyClass.$getComputedThrowing(this.$memoryAddress()); + } + """, + """ + private static native long $getComputedThrowing(long self); + """, + ] + ) + } + + @Test + func computedThrowing_swiftThunks() throws { + try assertOutput( + input: membersSource, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_MyClass__00024getComputedThrowing__J") + func Java_com_example_swift_MyClass__00024getComputedThrowing__J(environment: UnsafeMutablePointer!, thisClass: jclass, self: jlong) -> jlong { + ... + do { + return try self$.pointee.computedThrowing.getJNIValue(in: environment) + } catch { + environment.throwAsException(error) + return Int64.jniPlaceholderValue + } + } + """, + ] + ) + } + + @Test + func getterAndSetter_javaBindings() throws { + try assertOutput( + input: membersSource, + .jni, + .java, + detectChunkByInitialLines: 8, + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public var getterAndSetter: Int64 + * } + */ + public long getGetterAndSetter() { + return MyClass.$getGetterAndSetter(this.$memoryAddress()); + } + """, + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public var getterAndSetter: Int64 + * } + */ + public void setGetterAndSetter(long newValue) { + MyClass.$setGetterAndSetter(newValue, this.$memoryAddress()); + } + """, + """ + private static native long $getGetterAndSetter(long self); + """, + """ + private static native void $setGetterAndSetter(long newValue, long self); + """ + ] + ) + } + + @Test + func getterAndSetter_swiftThunks() throws { + try assertOutput( + input: membersSource, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_MyClass__00024getGetterAndSetter__J") + func Java_com_example_swift_MyClass__00024getGetterAndSetter__J(environment: UnsafeMutablePointer!, thisClass: jclass, self: jlong) -> jlong { + ... + return self$.pointee.getterAndSetter.getJNIValue(in: environment) + } + """, + """ + @_cdecl("Java_com_example_swift_MyClass__00024setGetterAndSetter__JJ") + func Java_com_example_swift_MyClass__00024setGetterAndSetter__JJ(environment: UnsafeMutablePointer!, thisClass: jclass, newValue: jlong, self: jlong) { + ... + self$.pointee.getterAndSetter = Int64(fromJNI: newValue, in: environment) + } + """ + ] + ) + } + + @Test + func someBoolean_javaBindings() throws { + try assertOutput( + input: membersSource, + .jni, + .java, + detectChunkByInitialLines: 8, + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public var someBoolean: Bool + * } + */ + public boolean isSomeBoolean() { + return MyClass.$isSomeBoolean(this.$memoryAddress()); + } + """, + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public var someBoolean: Bool + * } + */ + public void setSomeBoolean(boolean newValue) { + MyClass.$setSomeBoolean(newValue, this.$memoryAddress()); + } + """, + """ + private static native boolean $isSomeBoolean(long self); + """, + """ + private static native void $setSomeBoolean(boolean newValue, long self); + """ + ] + ) + } + + @Test + func someBoolean_swiftThunks() throws { + try assertOutput( + input: membersSource, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_MyClass__00024isSomeBoolean__J") + func Java_com_example_swift_MyClass__00024isSomeBoolean__J(environment: UnsafeMutablePointer!, thisClass: jclass, self: jlong) -> jboolean { + ... + return self$.pointee.someBoolean.getJNIValue(in: environment) + } + """, + """ + @_cdecl("Java_com_example_swift_MyClass__00024setSomeBoolean__ZJ") + func Java_com_example_swift_MyClass__00024setSomeBoolean__ZJ(environment: UnsafeMutablePointer!, thisClass: jclass, newValue: jboolean, self: jlong) { + ... + self$.pointee.someBoolean = Bool(fromJNI: newValue, in: environment) + } + """ + ] + ) + } + + @Test + func isBoolean_javaBindings() throws { + try assertOutput( + input: membersSource, + .jni, + .java, + detectChunkByInitialLines: 8, + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public var isBoolean: Bool + * } + */ + public boolean isBoolean() { + return MyClass.$isBoolean(this.$memoryAddress()); + } + """, + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public var isBoolean: Bool + * } + */ + public void setBoolean(boolean newValue) { + MyClass.$setBoolean(newValue, this.$memoryAddress()); + } + """, + """ + private static native boolean $isBoolean(long self); + """, + """ + private static native void $setBoolean(boolean newValue, long self); + """ + ] + ) + } + + @Test + func isBoolean_swiftThunks() throws { + try assertOutput( + input: membersSource, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_MyClass__00024isBoolean__J") + func Java_com_example_swift_MyClass__00024isBoolean__J(environment: UnsafeMutablePointer!, thisClass: jclass, self: jlong) -> jboolean { + ... + return self$.pointee.isBoolean.getJNIValue(in: environment) + } + """, + """ + @_cdecl("Java_com_example_swift_MyClass__00024setBoolean__ZJ") + func Java_com_example_swift_MyClass__00024setBoolean__ZJ(environment: UnsafeMutablePointer!, thisClass: jclass, newValue: jboolean, self: jlong) { + ... + self$.pointee.isBoolean = Bool(fromJNI: newValue, in: environment) + } + """ + ] + ) + } +} diff --git a/Tests/JExtractSwiftTests/MemoryManagementModeTests.swift b/Tests/JExtractSwiftTests/MemoryManagementModeTests.swift new file mode 100644 index 000000000..2228aad88 --- /dev/null +++ b/Tests/JExtractSwiftTests/MemoryManagementModeTests.swift @@ -0,0 +1,107 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JExtractSwiftLib +import SwiftJavaConfigurationShared +import Testing + +@Suite +struct MemoryManagementModeTests { + let text = + """ + class MyClass {} + + public func f() -> MyClass + """ + + @Test + func explicit() throws { + var config = Configuration() + config.memoryManagementMode = .explicit + + try assertOutput( + input: text, + config: config, + .jni, .java, + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func f() -> MyClass + * } + */ + public static MyClass f(SwiftArena swiftArena$) { + return MyClass.wrapMemoryAddressUnsafe(SwiftModule.$f(), swiftArena$); + } + """, + ] + ) + } + + @Test + func allowGlobalAutomatic() throws { + var config = Configuration() + config.memoryManagementMode = .allowGlobalAutomatic + + try assertOutput( + input: text, + config: config, + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public static MyClass f() { + return f(SwiftMemoryManagement.GLOBAL_SWIFT_JAVA_ARENA); + } + """, + """ + public static MyClass f(SwiftArena swiftArena$) { + return MyClass.wrapMemoryAddressUnsafe(SwiftModule.$f(), swiftArena$); + } + """, + ] + ) + } + + @Test + func allowGlobalAutomatic_protocol() throws { + var config = Configuration() + config.memoryManagementMode = .allowGlobalAutomatic + + try assertOutput( + input: + """ + public class MyClass {} + + public protocol MyProtocol { + public func f() -> MyClass + } + """, + config: config, + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public default MyClass f() { + return f(SwiftMemoryManagement.GLOBAL_SWIFT_JAVA_ARENA); + } + """, + """ + public MyClass f(SwiftArena swiftArena$); + """ + ] + ) + } +} diff --git a/Tests/JExtractSwiftTests/MethodImportTests.swift b/Tests/JExtractSwiftTests/MethodImportTests.swift index cd2bc1501..6ba930162 100644 --- a/Tests/JExtractSwiftTests/MethodImportTests.swift +++ b/Tests/JExtractSwiftTests/MethodImportTests.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import JExtractSwiftLib +import SwiftJavaConfigurationShared import Testing final class MethodImportTests { @@ -40,6 +41,8 @@ final class MethodImportTests { ) public func globalReturnClass() -> MySwiftClass + + public func swapRawBufferPointer(buffer: UnsafeRawBufferPointer) -> UnsafeMutableRawBufferPointer extension MySwiftClass { public func helloMemberInExtension() @@ -62,14 +65,15 @@ final class MethodImportTests { @Test("Import: public func helloWorld()") func method_helloWorld() throws { - let st = Swift2JavaTranslator( - swiftModuleName: "__FakeModule" - ) + var config = Configuration() + config.swiftModule = "__FakeModule" + let st = Swift2JavaTranslator(config: config) st.log.logLevel = .error - try st.analyze(file: "Fake.swift", text: class_interfaceFile) + try st.analyze(path: "Fake.swift", text: class_interfaceFile) let generator = FFMSwift2JavaGenerator( + config: config, translator: st, javaPackage: "com.example.swift", swiftOutputDirectory: "/fake", @@ -101,18 +105,19 @@ final class MethodImportTests { @Test("Import: public func globalTakeInt(i: Int)") func func_globalTakeInt() throws { - let st = Swift2JavaTranslator( - swiftModuleName: "__FakeModule" - ) + var config = Configuration() + config.swiftModule = "__FakeModule" + let st = Swift2JavaTranslator(config: config) st.log.logLevel = .error - try st.analyze(file: "Fake.swift", text: class_interfaceFile) + try st.analyze(path: "Fake.swift", text: class_interfaceFile) let funcDecl = st.importedGlobalFuncs.first { $0.name == "globalTakeInt" }! let generator = FFMSwift2JavaGenerator( + config: config, translator: st, javaPackage: "com.example.swift", swiftOutputDirectory: "/fake", @@ -142,18 +147,19 @@ final class MethodImportTests { @Test("Import: public func globalTakeIntLongString(i32: Int32, l: Int64, s: String)") func func_globalTakeIntLongString() throws { - let st = Swift2JavaTranslator( - swiftModuleName: "__FakeModule" - ) + var config = Configuration() + config.swiftModule = "__FakeModule" + let st = Swift2JavaTranslator(config: config) st.log.logLevel = .error - try st.analyze(file: "Fake.swift", text: class_interfaceFile) + try st.analyze(path: "Fake.swift", text: class_interfaceFile) let funcDecl = st.importedGlobalFuncs.first { $0.name == "globalTakeIntLongString" }! let generator = FFMSwift2JavaGenerator( + config: config, translator: st, javaPackage: "com.example.swift", swiftOutputDirectory: "/fake", @@ -165,7 +171,6 @@ final class MethodImportTests { } assertOutput( - dump: true, output, expected: """ @@ -177,7 +182,7 @@ final class MethodImportTests { */ public static void globalTakeIntLongString(int i32, long l, java.lang.String s) { try(var arena$ = Arena.ofConfined()) { - swiftjava___FakeModule_globalTakeIntLongString_i32_l_s.call(i32, l, SwiftKit.toCString(s, arena$)); + swiftjava___FakeModule_globalTakeIntLongString_i32_l_s.call(i32, l, SwiftRuntime.toCString(s, arena$)); } } """ @@ -186,18 +191,19 @@ final class MethodImportTests { @Test("Import: public func globalReturnClass() -> MySwiftClass") func func_globalReturnClass() throws { - let st = Swift2JavaTranslator( - swiftModuleName: "__FakeModule" - ) + var config = Configuration() + config.swiftModule = "__FakeModule" + let st = Swift2JavaTranslator(config: config) st.log.logLevel = .error - try st.analyze(file: "Fake.swift", text: class_interfaceFile) + try st.analyze(path: "Fake.swift", text: class_interfaceFile) let funcDecl = st.importedGlobalFuncs.first { $0.name == "globalReturnClass" }! let generator = FFMSwift2JavaGenerator( + config: config, translator: st, javaPackage: "com.example.swift", swiftOutputDirectory: "/fake", @@ -209,7 +215,6 @@ final class MethodImportTests { } assertOutput( - dump: true, output, expected: """ @@ -219,10 +224,57 @@ final class MethodImportTests { * public func globalReturnClass() -> MySwiftClass * } */ - public static MySwiftClass globalReturnClass(SwiftArena swiftArena$) { + public static MySwiftClass globalReturnClass(AllocatingSwiftArena swiftArena$) { MemorySegment _result = swiftArena$.allocate(MySwiftClass.$LAYOUT); swiftjava___FakeModule_globalReturnClass.call(_result); - return new MySwiftClass(_result, swiftArena$); + return MySwiftClass.wrapMemoryAddressUnsafe(_result, swiftArena$); + } + """ + ) + } + + @Test("Import: func swapRawBufferPointer(buffer: _)") + func func_globalSwapRawBufferPointer() throws { + var config = Configuration() + config.swiftModule = "__FakeModule" + let st = Swift2JavaTranslator(config: config) + st.log.logLevel = .error + + try st.analyze(path: "Fake.swift", text: class_interfaceFile) + + let funcDecl = st.importedGlobalFuncs.first { + $0.name == "swapRawBufferPointer" + }! + + let generator = FFMSwift2JavaGenerator( + config: config, + translator: st, + javaPackage: "com.example.swift", + swiftOutputDirectory: "/fake", + javaOutputDirectory: "/fake" + ) + + let output = CodePrinter.toString { printer in + generator.printJavaBindingWrapperMethod(&printer, funcDecl) + } + + assertOutput( + output, + expected: + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func swapRawBufferPointer(buffer: UnsafeRawBufferPointer) -> UnsafeMutableRawBufferPointer + * } + */ + public static java.lang.foreign.MemorySegment swapRawBufferPointer(java.lang.foreign.MemorySegment buffer) { + try(var arena$ = Arena.ofConfined()) { + MemorySegment _result_pointer = arena$.allocate(SwiftValueLayout.SWIFT_POINTER); + MemorySegment _result_count = arena$.allocate(SwiftValueLayout.SWIFT_INT64); + swiftjava___FakeModule_swapRawBufferPointer_buffer.call(buffer, buffer.byteSize(), _result_pointer, _result_count); + return _result_pointer.get(SwiftValueLayout.SWIFT_POINTER, 0).reinterpret(_result_count.get(SwiftValueLayout.SWIFT_INT64, 0)); + } } """ ) @@ -230,18 +282,19 @@ final class MethodImportTests { @Test func method_class_helloMemberFunction() throws { - let st = Swift2JavaTranslator( - swiftModuleName: "__FakeModule" - ) + var config = Configuration() + config.swiftModule = "__FakeModule" + let st = Swift2JavaTranslator(config: config) st.log.logLevel = .error - try st.analyze(file: "Fake.swift", text: class_interfaceFile) + try st.analyze(path: "Fake.swift", text: class_interfaceFile) let funcDecl: ImportedFunc = st.importedTypes["MySwiftClass"]!.methods.first { $0.name == "helloMemberFunction" }! let generator = FFMSwift2JavaGenerator( + config: config, translator: st, javaPackage: "com.example.swift", swiftOutputDirectory: "/fake", @@ -272,18 +325,19 @@ final class MethodImportTests { @Test func method_class_makeInt() throws { - let st = Swift2JavaTranslator( - swiftModuleName: "__FakeModule" - ) + var config = Configuration() + config.swiftModule = "__FakeModule" + let st = Swift2JavaTranslator(config: config) st.log.logLevel = .info - try st.analyze(file: "Fake.swift", text: class_interfaceFile) + try st.analyze(path: "Fake.swift", text: class_interfaceFile) let funcDecl: ImportedFunc = st.importedTypes["MySwiftClass"]!.methods.first { $0.name == "makeInt" }! let generator = FFMSwift2JavaGenerator( + config: config, translator: st, javaPackage: "com.example.swift", swiftOutputDirectory: "/fake", @@ -314,18 +368,19 @@ final class MethodImportTests { @Test func class_constructor() throws { - let st = Swift2JavaTranslator( - swiftModuleName: "__FakeModule" - ) + var config = Configuration() + config.swiftModule = "__FakeModule" + let st = Swift2JavaTranslator(config: config) st.log.logLevel = .info - try st.analyze(file: "Fake.swift", text: class_interfaceFile) + try st.analyze(path: "Fake.swift", text: class_interfaceFile) let initDecl: ImportedFunc = st.importedTypes["MySwiftClass"]!.initializers.first { $0.name == "init" }! let generator = FFMSwift2JavaGenerator( + config: config, translator: st, javaPackage: "com.example.swift", swiftOutputDirectory: "/fake", @@ -346,10 +401,10 @@ final class MethodImportTests { * public init(len: Swift.Int, cap: Swift.Int) * } */ - public static MySwiftClass init(long len, long cap, SwiftArena swiftArena$) { + public static MySwiftClass init(long len, long cap, AllocatingSwiftArena swiftArena$) { MemorySegment _result = swiftArena$.allocate(MySwiftClass.$LAYOUT); swiftjava___FakeModule_MySwiftClass_init_len_cap.call(len, cap, _result) - return new MySwiftClass(_result, swiftArena$); + return MySwiftClass.wrapMemoryAddressUnsafe(_result, swiftArena$); } """ ) @@ -357,18 +412,20 @@ final class MethodImportTests { @Test func struct_constructor() throws { - let st = Swift2JavaTranslator( - swiftModuleName: "__FakeModule" - ) + var config = Configuration() + config.swiftModule = "__FakeModule" + let st = Swift2JavaTranslator(config: config) + st.log.logLevel = .info - try st.analyze(file: "Fake.swift", text: class_interfaceFile) + try st.analyze(path: "Fake.swift", text: class_interfaceFile) let initDecl: ImportedFunc = st.importedTypes["MySwiftStruct"]!.initializers.first { $0.name == "init" }! let generator = FFMSwift2JavaGenerator( + config: config, translator: st, javaPackage: "com.example.swift", swiftOutputDirectory: "/fake", @@ -389,10 +446,10 @@ final class MethodImportTests { * public init(len: Swift.Int, cap: Swift.Int) * } */ - public static MySwiftStruct init(long len, long cap, SwiftArena swiftArena$) { + public static MySwiftStruct init(long len, long cap, AllocatingSwiftArena swiftArena$) { MemorySegment _result = swiftArena$.allocate(MySwiftStruct.$LAYOUT); swiftjava___FakeModule_MySwiftStruct_init_len_cap.call(len, cap, _result) - return new MySwiftStruct(_result, swiftArena$); + return MySwiftStruct.wrapMemoryAddressUnsafe(_result, swiftArena$); } """ ) diff --git a/Tests/JExtractSwiftTests/MethodThunkTests.swift b/Tests/JExtractSwiftTests/MethodThunkTests.swift index b91c0d8fb..dab550b3e 100644 --- a/Tests/JExtractSwiftTests/MethodThunkTests.swift +++ b/Tests/JExtractSwiftTests/MethodThunkTests.swift @@ -32,13 +32,9 @@ final class MethodThunkTests { @Test("Thunk overloads: globalFunc(a: Int32, b: Int64) & globalFunc(i32: Int32, l: Int64)") func thunk_overloads() throws { - let st = Swift2JavaTranslator( - swiftModuleName: "FakeModule" - ) - st.log.logLevel = .error - try assertOutput( - st, input: input, .swift, + input: input, .ffm, .swift, + swiftModuleName: "FakeModule", detectChunkByInitialLines: 1, expectedChunks: [ diff --git a/Tests/JExtractSwiftTests/OptionalImportTests.swift b/Tests/JExtractSwiftTests/OptionalImportTests.swift new file mode 100644 index 000000000..daa9fe664 --- /dev/null +++ b/Tests/JExtractSwiftTests/OptionalImportTests.swift @@ -0,0 +1,156 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JExtractSwiftLib +import Testing + +final class OptionalImportTests { + let interfaceFile = + """ + import Foundation + + public func receiveOptionalIntSugar(_ arg: Int?) + public func receiveOptionalIntExplicit(_ arg: Optional) + public func receiveOptionalDataProto(_ arg: (some DataProtocol)?)) + """ + + + @Test("Import Optionals: JavaBindings") + func data_javaBindings() throws { + + try assertOutput( + input: interfaceFile, .ffm, .java, + expectedChunks: [ + """ + /** + * {@snippet lang=c : + * void swiftjava_SwiftModule_receiveOptionalIntSugar__(const ptrdiff_t *arg) + * } + */ + private static class swiftjava_SwiftModule_receiveOptionalIntSugar__ { + private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + /* arg: */SwiftValueLayout.SWIFT_POINTER + ); + private static final MemorySegment ADDR = + SwiftModule.findOrThrow("swiftjava_SwiftModule_receiveOptionalIntSugar__"); + private static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + public static void call(java.lang.foreign.MemorySegment arg) { + try { + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall(arg); + } + HANDLE.invokeExact(arg); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + } + """, + + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func receiveOptionalIntSugar(_ arg: Int?) + * } + */ + public static void receiveOptionalIntSugar(OptionalLong arg) { + try(var arena$ = Arena.ofConfined()) { + swiftjava_SwiftModule_receiveOptionalIntSugar__.call(SwiftRuntime.toOptionalSegmentLong(arg, arena$)); + } + } + """, + + """ + /** + * {@snippet lang=c : + * void swiftjava_SwiftModule_receiveOptionalIntExplicit__(const ptrdiff_t *arg) + * } + */ + private static class swiftjava_SwiftModule_receiveOptionalIntExplicit__ { + private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + /* arg: */SwiftValueLayout.SWIFT_POINTER + ); + private static final MemorySegment ADDR = + SwiftModule.findOrThrow("swiftjava_SwiftModule_receiveOptionalIntExplicit__"); + private static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + public static void call(java.lang.foreign.MemorySegment arg) { + try { + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall(arg); + } + HANDLE.invokeExact(arg); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + } + """, + + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func receiveOptionalIntExplicit(_ arg: Optional) + * } + */ + public static void receiveOptionalIntExplicit(OptionalLong arg) { + try(var arena$ = Arena.ofConfined()) { + swiftjava_SwiftModule_receiveOptionalIntExplicit__.call(SwiftRuntime.toOptionalSegmentLong(arg, arena$)); + } + } + """, + + + """ + /** + * {@snippet lang=c : + * void swiftjava_SwiftModule_receiveOptionalDataProto__(const void *arg) + * } + */ + private static class swiftjava_SwiftModule_receiveOptionalDataProto__ { + private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + /* arg: */SwiftValueLayout.SWIFT_POINTER + ); + private static final MemorySegment ADDR = + SwiftModule.findOrThrow("swiftjava_SwiftModule_receiveOptionalDataProto__"); + private static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + public static void call(java.lang.foreign.MemorySegment arg) { + try { + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall(arg); + } + HANDLE.invokeExact(arg); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + } + """, + + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func receiveOptionalDataProto(_ arg: (some DataProtocol)?) + * } + */ + public static void receiveOptionalDataProto(Optional arg) { + swiftjava_SwiftModule_receiveOptionalDataProto__.call(SwiftRuntime.toOptionalSegmentInstance(arg)); + } + """, + ] + ) + } +} diff --git a/Tests/JExtractSwiftTests/SendableTests.swift b/Tests/JExtractSwiftTests/SendableTests.swift new file mode 100644 index 000000000..5116fc030 --- /dev/null +++ b/Tests/JExtractSwiftTests/SendableTests.swift @@ -0,0 +1,56 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JExtractSwiftLib +import Testing + +final class SendableTests { + let source = + """ + public struct SendableStruct: Sendable {} + """ + + + @Test("Import: Sendable struct (ffm)") + func sendableStruct_ffm() throws { + + try assertOutput( + input: source, .ffm, .java, + expectedChunks: [ + """ + @ThreadSafe // Sendable + public final class SendableStruct extends FFMSwiftInstance implements SwiftValue { + static final String LIB_NAME = "SwiftModule"; + static final Arena LIBRARY_ARENA = Arena.ofAuto(); + """, + ] + ) + } + + @Test("Import: Sendable struct (jni)") + func sendableStruct_jni() throws { + + try assertOutput( + input: source, .jni, .java, + expectedChunks: [ + """ + @ThreadSafe // Sendable + public final class SendableStruct implements JNISwiftInstance { + static final String LIB_NAME = "SwiftModule"; + """, + ] + ) + } + +} diff --git a/Tests/JExtractSwiftTests/StringPassingTests.swift b/Tests/JExtractSwiftTests/StringPassingTests.swift index 92ce8ea6b..ea81ac847 100644 --- a/Tests/JExtractSwiftTests/StringPassingTests.swift +++ b/Tests/JExtractSwiftTests/StringPassingTests.swift @@ -25,13 +25,9 @@ final class StringPassingTests { @Test("Import: public func writeString(string: String) -> Int") func method_helloWorld() throws { - let st = Swift2JavaTranslator( - swiftModuleName: "__FakeModule" - ) - st.log.logLevel = .trace - try assertOutput( - st, input: class_interfaceFile, .java, + input: class_interfaceFile, .ffm, .java, + swiftModuleName: "__FakeModule", expectedChunks: [ """ /** @@ -49,8 +45,8 @@ final class StringPassingTests { private static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); public static long call(java.lang.foreign.MemorySegment string) { try { - if (SwiftKit.TRACE_DOWNCALLS) { - SwiftKit.traceDowncall(string); + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall(string); } return (long) HANDLE.invokeExact(string); } catch (Throwable ex$) { @@ -68,7 +64,7 @@ final class StringPassingTests { */ public static long writeString(java.lang.String string) { try(var arena$ = Arena.ofConfined()) { - return swiftjava___FakeModule_writeString_string.call(SwiftKit.toCString(string, arena$)); + return swiftjava___FakeModule_writeString_string.call(SwiftRuntime.toCString(string, arena$)); } } """ diff --git a/Tests/JExtractSwiftTests/SwiftSymbolTableTests.swift b/Tests/JExtractSwiftTests/SwiftSymbolTableTests.swift index 2bbaa913e..b437454c5 100644 --- a/Tests/JExtractSwiftTests/SwiftSymbolTableTests.swift +++ b/Tests/JExtractSwiftTests/SwiftSymbolTableTests.swift @@ -21,7 +21,6 @@ import Testing struct SwiftSymbolTableSuite { @Test func lookupBindingTests() throws { - let symbolTable = SwiftSymbolTable(parsedModuleName: "MyModule") let sourceFile1: SourceFileSyntax = """ extension X.Y { struct Z { } @@ -33,8 +32,14 @@ struct SwiftSymbolTableSuite { let sourceFile2: SourceFileSyntax = """ struct X {} """ - - symbolTable.setup([sourceFile1, sourceFile2]) + let symbolTable = SwiftSymbolTable.setup( + moduleName: "MyModule", + [ + .init(syntax: sourceFile1, path: "Fake.swift"), + .init(syntax: sourceFile2, path: "Fake2.swift") + ], + log: Logger(label: "swift-java", logLevel: .critical) + ) let x = try #require(symbolTable.lookupType("X", parent: nil)) let xy = try #require(symbolTable.lookupType("Y", parent: x)) diff --git a/Tests/JExtractSwiftTests/UnsignedNumberTests.swift b/Tests/JExtractSwiftTests/UnsignedNumberTests.swift new file mode 100644 index 000000000..f3f784098 --- /dev/null +++ b/Tests/JExtractSwiftTests/UnsignedNumberTests.swift @@ -0,0 +1,409 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JExtractSwiftLib +import SwiftJavaConfigurationShared +import Testing + +final class UnsignedNumberTests { + + @Test( + "Import: UInt16 (char)", + arguments: [ + ( + JExtractGenerationMode.ffm, + /* expected chunks */ + [ + """ + /** + * {@snippet lang=c : + * void swiftjava_SwiftModule_unsignedChar__(uint16_t arg) + * } + */ + private static class swiftjava_SwiftModule_unsignedChar__ { + private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + /* arg: */SwiftValueLayout.SWIFT_UINT16 + ); + """, + """ + public static void unsignedChar(@Unsigned char arg) { + swiftjava_SwiftModule_unsignedChar__.call(arg); + } + """, + ] + ), + ( + JExtractGenerationMode.jni, + /* expected chunks */ + [ + """ + public static void unsignedChar(@Unsigned char arg) { + SwiftModule.$unsignedChar(arg); + } + private static native void $unsignedChar(char arg); + """, + ] + ) + ]) + func unsignedChar(mode: JExtractGenerationMode, expectedChunks: [String]) throws { + try assertOutput( + input: "public func unsignedChar(_ arg: UInt16)", + mode, .java, + detectChunkByInitialLines: 2, + expectedChunks: expectedChunks + ) + } + + @Test( + "Import: UInt32 (wrap)", + arguments: [ + ( + JExtractGenerationMode.ffm, + /* expected chunks */ + [ + """ + /** + * {@snippet lang=c : + * void swiftjava_SwiftModule_unsignedInt__(uint32_t arg) + * } + */ + private static class swiftjava_SwiftModule_unsignedInt__ { + private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + /* arg: */SwiftValueLayout.SWIFT_UINT32 + ); + """, + """ + public static void unsignedInt(com.google.common.primitives.UnsignedInteger arg) { + swiftjava_SwiftModule_unsignedInt__.call(UnsignedNumbers.toPrimitive(arg)); + } + """, + ] + ), + // JNI mode does not support the "wrap" mode + ]) + func unsignedInt_wrap(mode: JExtractGenerationMode, expectedChunks: [String]) throws { + var config = Configuration() + config.unsignedNumbersMode = .wrapGuava + + try assertOutput( + input: "public func unsignedInt(_ arg: UInt32)", + config: config, + mode, .java, + detectChunkByInitialLines: 2, + expectedChunks: expectedChunks + ) + } + + @Test( + "Import: UInt32 (annotate)", + arguments: [ + ( + JExtractGenerationMode.ffm, + /* expected chunks */ + [ + """ + /** + * {@snippet lang=c : + * void swiftjava_SwiftModule_unsignedInt__(uint32_t arg) + * } + */ + private static class swiftjava_SwiftModule_unsignedInt__ { + private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + /* arg: */SwiftValueLayout.SWIFT_UINT32 + ); + """, + """ + public static void unsignedInt(@Unsigned int arg) { + swiftjava_SwiftModule_unsignedInt__.call(arg); + } + """, + ] + ), + ( + JExtractGenerationMode.jni, + /* expected chunks */ + [ + """ + public static void unsignedInt(@Unsigned int arg) { + SwiftModule.$unsignedInt(arg); + } + private static native void $unsignedInt(int arg); + """, + ] + ) + ]) + func unsignedIntAnnotate(mode: JExtractGenerationMode, expectedChunks: [String]) throws { + var config = Configuration() + config.unsignedNumbersMode = .annotate + + try assertOutput( + input: "public func unsignedInt(_ arg: UInt32)", + config: config, + mode, .java, + detectChunkByInitialLines: 2, + expectedChunks: expectedChunks + ) + } + + @Test( + "Import: return UInt32 (default)", + arguments: [ + ( + JExtractGenerationMode.ffm, + /* expected chunks */ + [ + """ + /** + * {@snippet lang=c : + * uint32_t swiftjava_SwiftModule_returnUnsignedInt(void) + * } + */ + private static class swiftjava_SwiftModule_returnUnsignedInt { + private static final FunctionDescriptor DESC = FunctionDescriptor.of( + /* -> */SwiftValueLayout.SWIFT_UINT32 + ); + """, + """ + @Unsigned + public static int returnUnsignedInt() { + return swiftjava_SwiftModule_returnUnsignedInt.call(); + } + """, + ] + ), + ( + JExtractGenerationMode.jni, + /* expected chunks */ + [ + """ + @Unsigned + public static int returnUnsignedInt() { + return SwiftModule.$returnUnsignedInt(); + } + private static native int $returnUnsignedInt(); + """, + ] + ) + ]) + func returnUnsignedIntDefault(mode: JExtractGenerationMode, expectedChunks: [String]) throws { + let config = Configuration() + + try assertOutput( + input: "public func returnUnsignedInt() -> UInt32", + config: config, + mode, .java, + detectChunkByInitialLines: 2, + expectedChunks: expectedChunks + ) + } + + @Test( + "Import: return UInt64 (wrap)", + arguments: [ + ( + JExtractGenerationMode.ffm, + /* expected chunks */ + [ + """ + /** + * {@snippet lang=c : + * uint64_t swiftjava_SwiftModule_returnUnsignedLong(void) + * } + */ + private static class swiftjava_SwiftModule_returnUnsignedLong { + private static final FunctionDescriptor DESC = FunctionDescriptor.of( + /* -> */SwiftValueLayout.SWIFT_UINT64 + ); + """, + """ + public static com.google.common.primitives.UnsignedLong returnUnsignedLong() { + return com.google.common.primitives.UnsignedLong.fromLongBits(swiftjava_SwiftModule_returnUnsignedLong.call()); + } + """, + ] + ), + // JNI mode does not support "wrap" mode + ]) + func return_unsignedLongWrap(mode: JExtractGenerationMode, expectedChunks: [String]) throws { + var config = Configuration() + config.unsignedNumbersMode = .wrapGuava + + try assertOutput( + input: "public func returnUnsignedLong() -> UInt64", + config: config, + mode, .java, + detectChunkByInitialLines: 2, + expectedChunks: expectedChunks + ) + } + + @Test( + "Import: return UInt64 (annotate)", + arguments: [ + ( + JExtractGenerationMode.ffm, + /* expected chunks */ + [ + """ + /** + * {@snippet lang=c : + * uint64_t swiftjava_SwiftModule_returnUnsignedLong(void) + * } + */ + private static class swiftjava_SwiftModule_returnUnsignedLong { + private static final FunctionDescriptor DESC = FunctionDescriptor.of( + /* -> */SwiftValueLayout.SWIFT_UINT64 + ); + """, + """ + @Unsigned + public static long returnUnsignedLong() { + return swiftjava_SwiftModule_returnUnsignedLong.call(); + } + """, + ] + ), + ( + JExtractGenerationMode.jni, + /* expected chunks */ + [ + """ + @Unsigned + public static long returnUnsignedLong() { + return SwiftModule.$returnUnsignedLong(); + } + private static native long $returnUnsignedLong(); + """, + ] + ) + ]) + func return_unsignedLong_annotate(mode: JExtractGenerationMode, expectedChunks: [String]) throws { + var config = Configuration() + config.unsignedNumbersMode = .annotate + + try assertOutput( + input: "public func returnUnsignedLong() -> UInt64", + config: config, + mode, .java, + detectChunkByInitialLines: 2, + expectedChunks: expectedChunks + ) + } + + @Test( + "Import: take UInt64 (annotate)", + arguments: [ + ( + JExtractGenerationMode.ffm, + /* expected chunks */ + [ + """ + /** + * {@snippet lang=c : + * void swiftjava_SwiftModule_takeUnsignedLong_arg(uint64_t arg) + * } + */ + private static class swiftjava_SwiftModule_takeUnsignedLong_arg { + private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + /* arg: */SwiftValueLayout.SWIFT_UINT64 + ); + """, + """ + public static void takeUnsignedLong(@Unsigned long arg) { + swiftjava_SwiftModule_takeUnsignedLong_arg.call(arg); + } + """, + ] + ), + ( + JExtractGenerationMode.jni, + /* expected chunks */ + [ + """ + public static void takeUnsignedLong(@Unsigned long arg) { + SwiftModule.$takeUnsignedLong(arg); + } + private static native void $takeUnsignedLong(long arg); + """, + ] + ) + ]) + func take_unsignedLong_annotate(mode: JExtractGenerationMode, expectedChunks: [String]) throws { + var config = Configuration() + config.unsignedNumbersMode = .annotate + + try assertOutput( + input: "public func takeUnsignedLong(arg: UInt64)", + config: config, + mode, .java, + detectChunkByInitialLines: 2, + expectedChunks: expectedChunks + ) + } + + @Test( + "Import: take UInt64 return UInt32 (annotate)", + arguments: [ + ( + JExtractGenerationMode.ffm, + /* expected chunks */ + [ + """ + /** + * {@snippet lang=c : + * uint32_t swiftjava_SwiftModule_unsignedLong_first_second(uint64_t first, uint32_t second) + * } + */ + private static class swiftjava_SwiftModule_unsignedLong_first_second { + private static final FunctionDescriptor DESC = FunctionDescriptor.of( + /* -> */SwiftValueLayout.SWIFT_UINT32 + /* first: */SwiftValueLayout.SWIFT_UINT64 + /* second: */SwiftValueLayout.SWIFT_UINT32 + ); + """, + """ + @Unsigned + public static int unsignedLong(@Unsigned long first, @Unsigned int second) { + return swiftjava_SwiftModule_unsignedLong_first_second.call(first, second); + } + """, + ] + ), + ( + JExtractGenerationMode.jni, + /* expected chunks */ + [ + """ + @Unsigned + public static int unsignedLong(@Unsigned long first, @Unsigned int second) { + return SwiftModule.$unsignedLong(first, second); + } + private static native int $unsignedLong(long first, int second); + """, + ] + ), + ]) + func echo_unsignedLong_annotate(mode: JExtractGenerationMode, expectedChunks: [String]) throws { + var config = Configuration() + config.unsignedNumbersMode = .annotate + + try assertOutput( + input: "public func unsignedLong(first: UInt64, second: UInt32) -> UInt32", + config: config, + mode, .java, + detectChunkByInitialLines: 2, + expectedChunks: expectedChunks + ) + } +} diff --git a/Tests/JExtractSwiftTests/VariableImportTests.swift b/Tests/JExtractSwiftTests/VariableImportTests.swift index 562b92ae4..da0c1afaf 100644 --- a/Tests/JExtractSwiftTests/VariableImportTests.swift +++ b/Tests/JExtractSwiftTests/VariableImportTests.swift @@ -35,13 +35,9 @@ final class VariableImportTests { @Test("Import: var counter: Int") func variable_int() throws { - let st = Swift2JavaTranslator( - swiftModuleName: "FakeModule" - ) - st.log.logLevel = .error - try assertOutput( - st, input: class_interfaceFile, .java, + input: class_interfaceFile, .ffm, .java, + swiftModuleName: "FakeModule", detectChunkByInitialLines: 8, expectedChunks: [ """ @@ -55,8 +51,8 @@ final class VariableImportTests { private static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); public static long call(java.lang.foreign.MemorySegment self) { try { - if (SwiftKit.TRACE_DOWNCALLS) { - SwiftKit.traceDowncall(self); + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall(self); } return (long) HANDLE.invokeExact(self); } catch (Throwable ex$) { @@ -88,8 +84,8 @@ final class VariableImportTests { private static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); public static void call(long newValue, java.lang.foreign.MemorySegment self) { try { - if (SwiftKit.TRACE_DOWNCALLS) { - SwiftKit.traceDowncall(newValue, self); + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall(newValue, self); } HANDLE.invokeExact(newValue, self); } catch (Throwable ex$) { diff --git a/Tests/SwiftJavaConfigurationSharedTests/GradleDependencyParsingTests.swift b/Tests/SwiftJavaConfigurationSharedTests/GradleDependencyParsingTests.swift new file mode 100644 index 000000000..cb085fe22 --- /dev/null +++ b/Tests/SwiftJavaConfigurationSharedTests/GradleDependencyParsingTests.swift @@ -0,0 +1,45 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024-2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftJavaConfigurationShared +import Testing + +@Suite +struct GradleDependencyParsingTests { + + @Test + func parseSingleDependency() throws { + let inputString = "com.example:thing:12.2" + let parsed: JavaDependencyDescriptor = parseDependencyDescriptor(inputString)! + + #expect(parsed.groupID == "com.example") + #expect(parsed.artifactID == "thing") + #expect(parsed.version == "12.2") + } + + @Test + func parseMultiple() throws { + let inputString = "com.example:thing:12.2,com.example:another:1.2.3-beta.1," + let parsed: [JavaDependencyDescriptor] = parseDependencyDescriptors(inputString) + + #expect(parsed.count == 2) + #expect(parsed[0].groupID == "com.example") + #expect(parsed[0].artifactID == "thing") + #expect(parsed[0].version == "12.2") + #expect(parsed[1].groupID == "com.example") + #expect(parsed[1].artifactID == "another") + #expect(parsed[1].version == "1.2.3-beta.1") + } +} + diff --git a/Tests/SwiftJavaConfigurationSharedTests/SwiftJavaConfigurationTests.swift b/Tests/SwiftJavaConfigurationSharedTests/SwiftJavaConfigurationTests.swift new file mode 100644 index 000000000..c94669866 --- /dev/null +++ b/Tests/SwiftJavaConfigurationSharedTests/SwiftJavaConfigurationTests.swift @@ -0,0 +1,33 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024-2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftJavaConfigurationShared +import Testing + +@Suite +struct SwiftJavaConfigurationTests { + + @Test + func parseJSONWithComments() throws { + let config = try readConfiguration(string: + """ + // some comments + { + // anywhere is ok + "classpath": "" + } + """, configPath: nil) + } +} + diff --git a/Tests/JavaKitMacroTests/JavaClassMacroTests.swift b/Tests/SwiftJavaMacrosTests/JavaClassMacroTests.swift similarity index 89% rename from Tests/JavaKitMacroTests/JavaClassMacroTests.swift rename to Tests/SwiftJavaMacrosTests/JavaClassMacroTests.swift index 1dc08ba86..5cbeae162 100644 --- a/Tests/JavaKitMacroTests/JavaClassMacroTests.swift +++ b/Tests/SwiftJavaMacrosTests/JavaClassMacroTests.swift @@ -12,7 +12,7 @@ // //===----------------------------------------------------------------------===// -import JavaKitMacros +import SwiftJavaMacros import SwiftSyntax import SwiftSyntaxBuilder import SwiftSyntaxMacros @@ -296,5 +296,39 @@ class JavaKitMacroTests: XCTestCase { macros: Self.javaKitMacros ) } + + func testJavaOptionalGenericGet() throws { + assertMacroExpansion(""" + @JavaClass("java.lang.Optional") + open class JavaOptional: JavaObject { + @JavaMethod(typeErasedResult: "T") + open func get() -> T! + } + """, + expandedSource: """ + + open class JavaOptional: JavaObject { + open func get() -> T! { + /* convert erased return value to T */ + if let result$ = try! dynamicJavaMethodCall(methodName: "get", resultType: /*type-erased:T*/ JavaObject?.self) { + return T(javaThis: result$.javaThis, environment: try! JavaVirtualMachine.shared().environment()) + } else { + return nil + } + } + + /// The full Java class name for this Swift type. + open override class var fullJavaClassName: String { + "java.lang.Optional" + } + + public required init(javaHolder: JavaObjectHolder) { + super.init(javaHolder: javaHolder) + } + } + """, + macros: Self.javaKitMacros + ) + } } diff --git a/Tests/JavaKitTests/BasicRuntimeTests.swift b/Tests/SwiftJavaTests/BasicRuntimeTests.swift similarity index 88% rename from Tests/JavaKitTests/BasicRuntimeTests.swift rename to Tests/SwiftJavaTests/BasicRuntimeTests.swift index d42fa4b12..292c2a688 100644 --- a/Tests/JavaKitTests/BasicRuntimeTests.swift +++ b/Tests/SwiftJavaTests/BasicRuntimeTests.swift @@ -12,8 +12,8 @@ // //===----------------------------------------------------------------------===// -import JavaKit -import JavaKitNetwork +import SwiftJava +import JavaNet import XCTest // NOTE: Workaround for https://github.com/swiftlang/swift-java/issues/43 /// Handy reference to the JVM abstraction. @@ -82,6 +82,17 @@ class BasicRuntimeTests: XCTestCase { let nullString = String(fromJNI: nil, in: environment) XCTAssertEqual(nullString, "") } + + func testCrossThreadAccess() async throws { + let environment = try jvm.environment() + let url = try URL("https://swift.org", environment: environment) + let string = await Task.detached { + // This should be called on a different thread + url.toString() + }.value + + XCTAssertEqual(string, "https://swift.org") + } } @JavaClass("org.swift.javakit.Nonexistent") diff --git a/Tests/SwiftJavaToolLibTests/CompileJavaWrapTools.swift b/Tests/SwiftJavaToolLibTests/CompileJavaWrapTools.swift new file mode 100644 index 000000000..68702b621 --- /dev/null +++ b/Tests/SwiftJavaToolLibTests/CompileJavaWrapTools.swift @@ -0,0 +1,174 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@_spi(Testing) import SwiftJava +import SwiftJavaToolLib +import JavaUtilJar +import JavaNet +import SwiftJavaShared +import SwiftJavaConfigurationShared +import _Subprocess +import XCTest // NOTE: Workaround for https://github.com/swiftlang/swift-java/issues/43 +import Foundation + +fileprivate func createTemporaryDirectory(in directory: Foundation.URL) throws -> Foundation.URL { + let uuid = UUID().uuidString + let resolverDirectoryURL = directory.appendingPathComponent("swift-java-testing-\(uuid)") + + try FileManager.default.createDirectory(at: resolverDirectoryURL, withIntermediateDirectories: true, attributes: nil) + + return resolverDirectoryURL +} + +/// Returns the directory that should be added to the classpath of the JVM to analyze the sources. +func compileJava(_ sourceText: String) async throws -> Foundation.URL { + let sourceFile = try TempFile.create(suffix: "java", sourceText) + + let classesDirectory = try createTemporaryDirectory(in: FileManager.default.temporaryDirectory) + + let javacProcess = try await _Subprocess.run( + .path(.init("\(javaHome)" + "/bin/javac")), + arguments: [ + "-d", classesDirectory.path, // output directory for .class files + sourceFile.path + ], + output: .string(limit: Int.max, encoding: UTF8.self), + error: .string(limit: Int.max, encoding: UTF8.self) + ) + + // Check if compilation was successful + guard javacProcess.terminationStatus.isSuccess else { + let outString = javacProcess.standardOutput ?? "" + let errString = javacProcess.standardError ?? "" + fatalError("javac '\(sourceFile)' failed (\(javacProcess.terminationStatus));\n" + + "OUT: \(outString)\n" + + "ERROR: \(errString)") + } + + print("Compiled java sources to: \(classesDirectory)") + return classesDirectory +} + +func withJavaTranslator( + javaClassNames: [String], + classpath: [Foundation.URL], + body: (JavaTranslator) throws -> (), + function: String = #function, + file: StaticString = #filePath, + line: UInt = #line +) throws { + print("New withJavaTranslator, for classpath: \(classpath)") + let jvm = try JavaVirtualMachine.shared( + classpath: classpath.map(\.path), + replace: true + ) + + var config = Configuration() + config.minimumInputAccessLevelMode = .package + + let environment = try jvm.environment() + let translator = JavaTranslator( + config: config, + swiftModuleName: "SwiftModule", + environment: environment, + translateAsClass: true) + + try body(translator) +} + +/// Translate a Java class and assert that the translated output contains +/// each of the expected "chunks" of text. +func assertWrapJavaOutput( + javaClassNames: [String], + classpath: [Foundation.URL], + assert assertBody: (JavaTranslator) throws -> Void = { _ in }, + expectedChunks: [String], + function: String = #function, + file: StaticString = #filePath, + line: UInt = #line +) throws { + let jvm = try JavaVirtualMachine.shared( + //classpath: classpath.map(\.path), + replace: false + ) + // Do NOT destroy the jvm here, because the JavaClasses will need to deinit, + // and do so while the env is still valid... + + var config = Configuration() + config.minimumInputAccessLevelMode = .package + + let environment = try jvm.environment() + let translator = JavaTranslator( + config: config, + swiftModuleName: "SwiftModule", + environment: environment, + translateAsClass: true) + + let classpathJavaURLs = classpath.map({ try! URL.init("\($0)/") }) // we MUST have a trailing slash for JVM to consider it a search directory + let classLoader = URLClassLoader(classpathJavaURLs, environment: environment) + + // FIXME: deduplicate this + translator.startNewFile() + + var swiftCompleteOutputText = "" + + var javaClasses: [JavaClass] = [] + for javaClassName in javaClassNames { + guard let javaClass = try! classLoader.loadClass(javaClassName) else { + fatalError("Could not load Java class '\(javaClassName)' in test \(function) @ \(file):\(line)!") + } + javaClasses.append(javaClass) + + // FIXME: deduplicate this with SwiftJava.WrapJavaCommand.runCommand !!! + // TODO: especially because nested classes + // WrapJavaCommand(). + + let swiftUnqualifiedName = javaClassName.javaClassNameToCanonicalName + .defaultSwiftNameForJavaClass + translator.translatedClasses[javaClassName] = + .init(module: nil, name: swiftUnqualifiedName) + + try translator.validateClassConfiguration() + + let swiftClassDecls = try translator.translateClass(javaClass) + let importDecls = translator.getImportDecls() + + let swiftFileText = + """ + // --------------------------------------------------------------------------- + // Auto-generated by Java-to-Swift wrapper generator. + \(importDecls.map { $0.description }.joined()) + \(swiftClassDecls.map { $0.description }.joined(separator: "\n")) + \n + """ + swiftCompleteOutputText += swiftFileText + } + + // Run any additional user defined assertions: + try assertBody(translator) + + for expectedChunk in expectedChunks { + // We make the matching in-sensitive to whitespace: + let checkAgainstText = swiftCompleteOutputText.replacing(" ", with: "") + let checkAgainstExpectedChunk = expectedChunk.replacing(" ", with: "") + +let failureMessage = "Expected chunk: \n" + + "\(expectedChunk.yellow)" + + "\n" + + "not found in:\n" + + "\(swiftCompleteOutputText)" + XCTAssertTrue(checkAgainstText.contains(checkAgainstExpectedChunk), + "\(failureMessage)") + } +} \ No newline at end of file diff --git a/Tests/SwiftJavaTests/Java2SwiftTests.swift b/Tests/SwiftJavaToolLibTests/Java2SwiftTests.swift similarity index 60% rename from Tests/SwiftJavaTests/Java2SwiftTests.swift rename to Tests/SwiftJavaToolLibTests/Java2SwiftTests.swift index e2b68a344..a3b33ba75 100644 --- a/Tests/SwiftJavaTests/Java2SwiftTests.swift +++ b/Tests/SwiftJavaToolLibTests/Java2SwiftTests.swift @@ -13,8 +13,9 @@ //===----------------------------------------------------------------------===// @_spi(Testing) -import JavaKit -import SwiftJavaLib +import SwiftJava +import SwiftJavaConfigurationShared +import SwiftJavaToolLib import XCTest // NOTE: Workaround for https://github.com/swiftlang/swift-java/issues/43 /// Handy reference to the JVM abstraction. @@ -42,18 +43,18 @@ class Java2SwiftTests: XCTestCase { JavaObject.self, swiftTypeName: "MyJavaObject", expectedChunks: [ - "import JavaKit", + "import SwiftJava", """ @JavaClass("java.lang.Object") public struct MyJavaObject { """, """ - @JavaMethod - public func toString() -> String + @JavaMethod + public func toString() -> String """, """ - @JavaMethod - public func wait() throws + @JavaMethod + public func wait() throws """ ] ) @@ -64,17 +65,17 @@ class Java2SwiftTests: XCTestCase { JavaClass.self, swiftTypeName: "MyJavaClass", translatedClasses: [ - "java.lang.Object": ("JavaObject", nil), + "java.lang.Object": SwiftTypeName(module: nil, name: "JavaObject"), ], expectedChunks: [ - "import JavaKit", + "import SwiftJava", """ @JavaClass("java.lang.Class", extends: JavaObject.self) public struct MyJavaClass { """, """ - @JavaStaticMethod - public func forName(_ arg0: String) throws -> MyJavaClass! where ObjectType == MyJavaClass + @JavaStaticMethod + public func forName(_ arg0: String) throws -> MyJavaClass! where ObjectType == MyJavaClass """, ] ) @@ -85,7 +86,7 @@ class Java2SwiftTests: XCTestCase { JavaMonth.self, swiftTypeName: "Month", expectedChunks: [ - "import JavaKit", + "import SwiftJava", "enum MonthCases: Equatable", "case APRIL", "public var enumValue: MonthCases!", @@ -100,12 +101,12 @@ class Java2SwiftTests: XCTestCase { if let APRIL = classObj.APRIL { self = APRIL } else { - fatalError("Enum value APRIL was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value APRIL was unexpectedly nil, please re-run swift-java on the most updated Java class") } """, """ - @JavaStaticField(isFinal: true) - public var APRIL: Month! + @JavaStaticField(isFinal: true) + public var APRIL: Month! """ ]) } @@ -115,19 +116,19 @@ class Java2SwiftTests: XCTestCase { MyArrayList.self, swiftTypeName: "JavaArrayList", translatedClasses: [ - "java.lang.Object": ("JavaObject", nil), - "java.lang.reflect.Array": ("JavaArray", nil), - "java.util.List": ("JavaList", nil), - "java.util.function.IntFunction": ("MyJavaIntFunction", nil), + "java.lang.Object": SwiftTypeName(module: nil, name: "JavaObject"), + "java.lang.reflect.Array": SwiftTypeName(module: nil, name: "JavaArray"), + "java.util.List": SwiftTypeName(module: nil, name: "JavaList"), + "java.util.function.IntFunction": SwiftTypeName(module: nil, name: "MyJavaIntFunction"), ], expectedChunks: [ """ - @JavaMethod - public func subList(_ arg0: Int32, _ arg1: Int32) -> JavaList! + @JavaMethod + public func subList(_ arg0: Int32, _ arg1: Int32) -> JavaList! """, """ - @JavaMethod - public func toArray(_ arg0: MyJavaIntFunction?) -> [JavaObject?] + @JavaMethod + public func toArray(_ arg0: MyJavaIntFunction?) -> [T?] """ ] ) @@ -138,13 +139,13 @@ class Java2SwiftTests: XCTestCase { MyLinkedList.self, swiftTypeName: "JavaLinkedList", translatedClasses: [ - "java.lang.Object": ("JavaObject", nil), - "java.util.List": ("JavaList", nil), + "java.lang.Object": SwiftTypeName(module: nil, name: "JavaObject"), + "java.util.List": SwiftTypeName(module: nil, name: "JavaList"), ], expectedChunks: [ """ - @JavaMethod - public func subList(_ arg0: Int32, _ arg1: Int32) -> JavaList! + @JavaMethod + public func subList(_ arg0: Int32, _ arg1: Int32) -> JavaList! """ ] ) @@ -155,19 +156,19 @@ class Java2SwiftTests: XCTestCase { ProcessBuilder.self, swiftTypeName: "ProcessBuilder", translatedClasses: [ - "java.lang.ProcessBuilder": ("ProcessBuilder", nil), - "java.lang.ProcessBuilder$Redirect": ("ProcessBuilder.Redirect", nil), - "java.lang.ProcessBuilder$Redirect$Type": ("ProcessBuilder.Redirect.Type", nil), + "java.lang.ProcessBuilder": SwiftTypeName(module: nil, name: "ProcessBuilder"), + "java.lang.ProcessBuilder$Redirect": SwiftTypeName(module: nil, name: "ProcessBuilder.Redirect"), + "java.lang.ProcessBuilder$Redirect$Type": SwiftTypeName(module: nil, name: "ProcessBuilder.Redirect.Type"), ], nestedClasses: [ "java.lang.ProcessBuilder": [JavaClass().as(JavaClass.self)!], "java.lang.ProcessBuilder$Redirect": [JavaClass().as(JavaClass.self)!], ], expectedChunks: [ - "import JavaKit", + "import SwiftJava", """ - @JavaMethod - public func redirectInput() -> ProcessBuilder.Redirect! + @JavaMethod + public func redirectInput() -> ProcessBuilder.Redirect! """, """ extension ProcessBuilder { @@ -183,8 +184,8 @@ class Java2SwiftTests: XCTestCase { public struct Type { """, """ - @JavaMethod - public func type() -> ProcessBuilder.Redirect.`Type`! + @JavaMethod + public func type() -> ProcessBuilder.Redirect.`Type`! """, ] ) @@ -195,19 +196,19 @@ class Java2SwiftTests: XCTestCase { ProcessBuilder.self, swiftTypeName: "ProcessBuilder", translatedClasses: [ - "java.lang.ProcessBuilder": ("ProcessBuilder", nil), - "java.lang.ProcessBuilder$Redirect": ("ProcessBuilder.PBRedirect", nil), - "java.lang.ProcessBuilder$Redirect$Type": ("ProcessBuilder.PBRedirect.JavaType", nil), + "java.lang.ProcessBuilder": SwiftTypeName(module: nil, name: "ProcessBuilder"), + "java.lang.ProcessBuilder$Redirect": SwiftTypeName(module: nil, name: "ProcessBuilder.PBRedirect"), + "java.lang.ProcessBuilder$Redirect$Type": SwiftTypeName(module: nil, name: "ProcessBuilder.PBRedirect.JavaType"), ], nestedClasses: [ "java.lang.ProcessBuilder": [JavaClass().as(JavaClass.self)!], "java.lang.ProcessBuilder$Redirect": [JavaClass().as(JavaClass.self)!], ], expectedChunks: [ - "import JavaKit", + "import SwiftJava", """ - @JavaMethod - public func redirectInput() -> ProcessBuilder.PBRedirect! + @JavaMethod + public func redirectInput() -> ProcessBuilder.PBRedirect! """, """ extension ProcessBuilder { @@ -223,8 +224,8 @@ class Java2SwiftTests: XCTestCase { public struct JavaType { """, """ - @JavaMethod - public func type() -> ProcessBuilder.PBRedirect.JavaType! + @JavaMethod + public func type() -> ProcessBuilder.PBRedirect.JavaType! """ ] ) @@ -248,21 +249,21 @@ class Java2SwiftTests: XCTestCase { MyObjects.self, swiftTypeName: "MyJavaObjects", translatedClasses: [ - "java.lang.Object" : ("JavaObject", "JavaKit"), - "java.util.function.Supplier" : ("MySupplier", "JavaKitFunction"), - "java.lang.String" : ("JavaString", "JavaKit"), + "java.lang.Object" : SwiftTypeName(module: "SwiftJava", name: "JavaObject"), + "java.util.function.Supplier" : SwiftTypeName(module: "JavaUtilFunction", name: "MySupplier"), + "java.lang.String" : SwiftTypeName(module: "SwiftJava", name: "JavaString"), ], expectedChunks: [ """ - import JavaKitFunction + import JavaUtilFunction """, """ @JavaClass("java.util.Objects", extends: JavaObject.self) public struct MyJavaObjects { """, """ - @JavaStaticMethod - public func requireNonNull(_ arg0: JavaObject?, _ arg1: MySupplier?) -> JavaObject! + @JavaStaticMethod(typeErasedResult: "T!") + public func requireNonNull(_ arg0: T?, _ arg1: MySupplier?) -> T """, ] ) @@ -274,26 +275,26 @@ class Java2SwiftTests: XCTestCase { swiftTypeName: "JavaObject", asClass: true, expectedChunks: [ - "import JavaKit", + "import SwiftJava", """ @JavaClass("java.lang.Object") open class JavaObject { """, """ - @JavaMethod - @_nonoverride public convenience init(environment: JNIEnvironment? = nil) + @JavaMethod + @_nonoverride public convenience init(environment: JNIEnvironment? = nil) """, """ - @JavaMethod - open func toString() -> String + @JavaMethod + open func toString() -> String """, """ - @JavaMethod - open func wait() throws + @JavaMethod + open func wait() throws """, """ - @JavaMethod - open func clone() throws -> JavaObject! + @JavaMethod + open func clone() throws -> JavaObject! """, ] ) @@ -305,33 +306,33 @@ class Java2SwiftTests: XCTestCase { swiftTypeName: "JavaString", asClass: true, translatedClasses: [ - "java.lang.Object" : ("JavaObject", "JavaKit"), + "java.lang.Object" : SwiftTypeName(module: "SwiftJava", name: "JavaObject"), ], expectedChunks: [ - "import JavaKit", + "import SwiftJava", """ @JavaClass("java.lang.String") open class JavaString: JavaObject { """, """ - @JavaMethod - @_nonoverride public convenience init(environment: JNIEnvironment? = nil) + @JavaMethod + @_nonoverride public convenience init(environment: JNIEnvironment? = nil) """, """ - @JavaMethod - open override func toString() -> String + @JavaMethod + open override func toString() -> String """, """ - @JavaMethod - open override func equals(_ arg0: JavaObject?) -> Bool + @JavaMethod + open override func equals(_ arg0: JavaObject?) -> Bool """, """ - @JavaMethod - open func intern() -> String + @JavaMethod + open func intern() -> String """, """ - @JavaStaticMethod - public func valueOf(_ arg0: Int64) -> String + @JavaStaticMethod + public func valueOf(_ arg0: Int64) -> String """, ] ) @@ -343,7 +344,7 @@ class Java2SwiftTests: XCTestCase { swiftTypeName: "Month", asClass: true, expectedChunks: [ - "import JavaKit", + "import SwiftJava", "enum MonthCases: Equatable", "case APRIL", "public var enumValue: MonthCases!", @@ -361,12 +362,12 @@ class Java2SwiftTests: XCTestCase { if let APRIL = classObj.APRIL { self.init(javaHolder: APRIL.javaHolder) } else { - fatalError("Enum value APRIL was unexpectedly nil, please re-run Java2Swift on the most updated Java class") + fatalError("Enum value APRIL was unexpectedly nil, please re-run swift-java on the most updated Java class") } """, """ - @JavaStaticField(isFinal: true) - public var APRIL: Month! + @JavaStaticField(isFinal: true) + public var APRIL: Month! """ ]) } @@ -380,23 +381,23 @@ class Java2SwiftTests: XCTestCase { swiftTypeName: "URLClassLoader", asClass: true, translatedClasses: [ - "java.lang.Object" : ("JavaObject", "JavaKit"), - "java.lang.ClassLoader" : ("ClassLoader", "JavaKit"), - "java.net.URL" : ("URL", "JavaKitNetwork"), + "java.lang.Object" : SwiftTypeName(module: "SwiftJava", name: "JavaObject"), + "java.lang.ClassLoader" : SwiftTypeName(module: "SwiftJava", name: "ClassLoader"), + "java.net.URL" : SwiftTypeName(module: "JavaNet", name: "URL"), ], expectedChunks: [ - "import JavaKit", + "import SwiftJava", """ @JavaClass("java.net.URLClassLoader") open class URLClassLoader: ClassLoader { """, """ - @JavaMethod - open func close() throws + @JavaMethod + open func close() throws """, """ - @JavaMethod - open override func findResource(_ arg0: String) -> URL! + @JavaMethod + open override func findResource(_ arg0: String) -> URL! """, ] ) @@ -411,22 +412,22 @@ class Java2SwiftTests: XCTestCase { swiftTypeName: "URLClassLoader", asClass: true, translatedClasses: [ - "java.lang.Object" : ("JavaObject", "JavaKit"), - "java.net.URL" : ("URL", "JavaKitNetwork"), + "java.lang.Object": SwiftTypeName(module: "SwiftJava", name: "JavaObject"), + "java.net.URL": SwiftTypeName(module: "JavaNet", name: "URL"), ], expectedChunks: [ - "import JavaKit", + "import SwiftJava", """ @JavaClass("java.net.URLClassLoader") open class URLClassLoader: JavaObject { """, """ - @JavaMethod - open func close() throws + @JavaMethod + open func close() throws """, """ - @JavaMethod - open func findResource(_ arg0: String) -> URL! + @JavaMethod + open func findResource(_ arg0: String) -> URL! """, ] ) @@ -440,19 +441,19 @@ class Java2SwiftTests: XCTestCase { swiftTypeName: "JavaByte", asClass: true, translatedClasses: [ - "java.lang.Object" : ("JavaObject", "JavaKit"), - "java.lang.Number" : ("JavaNumber", "JavaKit"), - "java.lang.Byte" : ("JavaByte", "JavaKit"), + "java.lang.Object": SwiftTypeName(module: "SwiftJava", name: "JavaObject"), + "java.lang.Number": SwiftTypeName(module: "SwiftJava", name: "JavaNumber"), + "java.lang.Byte": SwiftTypeName(module: "SwiftJava", name: "JavaByte"), ], expectedChunks: [ - "import JavaKit", + "import SwiftJava", """ @JavaClass("java.lang.Byte") open class JavaByte: JavaNumber { """, """ - @JavaMethod - open override func equals(_ arg0: JavaObject?) -> Bool + @JavaMethod + open override func equals(_ arg0: JavaObject?) -> Bool """, ] ) @@ -464,18 +465,18 @@ class Java2SwiftTests: XCTestCase { swiftTypeName: "MyJavaIntFunction", asClass: true, translatedClasses: [ - "java.lang.Object" : ("JavaObject", "JavaKit"), - "java.util.function.IntFunction": ("MyJavaIntFunction", nil), + "java.lang.Object" : SwiftTypeName(module: "SwiftJava", name: "JavaObject"), + "java.util.function.IntFunction": SwiftTypeName(module: nil, name: "MyJavaIntFunction"), ], expectedChunks: [ - "import JavaKit", + "import SwiftJava", """ @JavaInterface("java.util.function.IntFunction") public struct MyJavaIntFunction { """, """ - @JavaMethod - public func apply(_ arg0: Int32) -> JavaObject! + @JavaMethod(typeErasedResult: "R!") + public func apply(_ arg0: Int32) -> R! """, ] ) @@ -487,29 +488,29 @@ class Java2SwiftTests: XCTestCase { swiftTypeName: "Method", asClass: true, translatedClasses: [ - "java.lang.Object" : ("JavaObject", "JavaKit"), - "java.lang.Class" : ("JavaClass", "JavaKit"), - "java.lang.reflect.Executable": ("Executable", "JavaKitReflection"), - "java.lang.reflect.Method": ("Method", "JavaKitReflection"), - "java.lang.reflect.TypeVariable" : ("TypeVariable", "JavaKitReflection"), + "java.lang.Object" : SwiftTypeName(module: "SwiftJava", name: "JavaObject"), + "java.lang.Class" : SwiftTypeName(module: "SwiftJava", name: "JavaClass"), + "java.lang.reflect.Executable": SwiftTypeName(module: "JavaLangReflect", name: "Executable"), + "java.lang.reflect.Method": SwiftTypeName(module: "JavaLangReflect", name: "Method"), + "java.lang.reflect.TypeVariable" : SwiftTypeName(module: "JavaLangReflect", name: "TypeVariable"), ], expectedChunks: [ - "import JavaKitReflection", + "import JavaLangReflect", """ @JavaClass("java.lang.reflect.Method") open class Method: Executable { """, """ - @JavaMethod - open func getTypeParameters() -> [TypeVariable?] + @JavaMethod + open func getTypeParameters() -> [TypeVariable?] """, """ - @JavaMethod - open override func getParameterTypes() -> [JavaClass?] + @JavaMethod + open override func getParameterTypes() -> [JavaClass?] """, """ - @JavaMethod - open override func getDeclaringClass() -> JavaClass! + @JavaMethod + open override func getDeclaringClass() -> JavaClass! """, ] ) @@ -521,29 +522,29 @@ class Java2SwiftTests: XCTestCase { swiftTypeName: "Constructor", asClass: true, translatedClasses: [ - "java.lang.Object" : ("JavaObject", "JavaKit"), - "java.lang.Class" : ("JavaClass", "JavaKit"), - "java.lang.reflect.Executable": ("Executable", "JavaKitReflection"), - "java.lang.reflect.Method": ("Method", "JavaKitReflection"), - "java.lang.reflect.TypeVariable" : ("TypeVariable", "JavaKitReflection"), + "java.lang.Object" : SwiftTypeName(module: "SwiftJava", name: "JavaObject"), + "java.lang.Class" : SwiftTypeName(module: "SwiftJava", name: "JavaClass"), + "java.lang.reflect.Executable": SwiftTypeName(module: "JavaLangReflect", name: "Executable"), + "java.lang.reflect.Method": SwiftTypeName(module: "JavaLangReflect", name: "Method"), + "java.lang.reflect.TypeVariable" : SwiftTypeName(module: "JavaLangReflect", name: "TypeVariable"), ], expectedChunks: [ - "import JavaKitReflection", + "import JavaLangReflect", """ @JavaClass("java.lang.reflect.Constructor") open class Constructor: Executable { """, """ - @JavaMethod - open func getTypeParameters() -> [TypeVariable>?] + @JavaMethod + open func getTypeParameters() -> [TypeVariable>?] """, """ - @JavaMethod - open override func getParameterTypes() -> [JavaClass?] + @JavaMethod + open override func getParameterTypes() -> [JavaClass?] """, """ - @JavaMethod - open override func getDeclaringClass() -> JavaClass! + @JavaMethod + open override func getDeclaringClass() -> JavaClass! """, ] ) @@ -555,13 +556,13 @@ class Java2SwiftTests: XCTestCase { swiftTypeName: "NIOByteBuffer", asClass: true, translatedClasses: [ - "java.lang.Object" : ("JavaObject", "JavaKit"), - "java.lang.Class" : ("JavaClass", "JavaKit"), - "java.nio.Buffer": ("NIOBuffer", "JavaKitNIO"), - "java.nio.ByteBuffer": ("NIOByteBuffer", "JavaKitNIO"), + "java.lang.Object": SwiftTypeName(module: "SwiftJava", name: "JavaObject"), + "java.lang.Class": SwiftTypeName(module: "SwiftJava", name: "JavaClass"), + "java.nio.Buffer": SwiftTypeName(module: "JavaNio", name: "NIOBuffer"), + "java.nio.ByteBuffer": SwiftTypeName(module: "JavaNio", name: "NIOByteBuffer"), ], expectedChunks: [ - "import JavaKitNIO", + "import JavaNio", """ @JavaClass("java.nio.ByteBuffer") open class NIOByteBuffer: NIOBuffer { @@ -580,61 +581,58 @@ class Java2SwiftTests: XCTestCase { } @JavaClass("java.lang.ClassLoader") -public struct ClassLoader { } +fileprivate struct ClassLoader { } @JavaClass("java.security.SecureClassLoader") -public struct SecureClassLoader { } +fileprivate struct SecureClassLoader { } @JavaClass("java.net.URLClassLoader") -public struct URLClassLoader { } - +fileprivate struct URLClassLoader { } @JavaClass("java.util.ArrayList") -public struct MyArrayList { +fileprivate struct MyArrayList { } @JavaClass("java.util.LinkedList") -public struct MyLinkedList { +fileprivate struct MyLinkedList { } @JavaClass("java.lang.String") -public struct MyJavaString { +fileprivate struct MyJavaString { } @JavaClass("java.util.Objects") -public struct MyObjects { } +fileprivate struct MyObjects { } @JavaInterface("java.util.function.Supplier") -public struct MySupplier { } +fileprivate struct MySupplier { } @JavaInterface("java.util.function.IntFunction") -public struct MyJavaIntFunction { +fileprivate struct MyJavaIntFunction { } @JavaClass("java.lang.reflect.Method", extends: Executable.self) -public struct Method { +fileprivate struct Method { } @JavaClass("java.lang.reflect.Constructor", extends: Executable.self) -public struct Constructor { +fileprivate struct Constructor { } @JavaClass("java.lang.reflect.Executable") -public struct Executable { +fileprivate struct Executable { } @JavaInterface("java.lang.reflect.TypeVariable") -public struct TypeVariable { +fileprivate struct TypeVariable { } @JavaClass("java.nio.Buffer") -open class NIOBuffer: JavaObject { - +fileprivate class NIOBuffer: JavaObject { } @JavaClass("java.nio.ByteBuffer") -open class NIOByteBuffer: NIOBuffer { - +fileprivate class NIOByteBuffer: NIOBuffer { } /// Translate a Java class and assert that the translated output contains @@ -643,9 +641,8 @@ func assertTranslatedClass( _ javaType: JavaClassType.Type, swiftTypeName: String, asClass: Bool = false, - translatedClasses: [ - String: (swiftType: String, swiftModule: String?) - ] = [:], + config: Configuration = Configuration(), + translatedClasses: [String: SwiftTypeName] = [:], nestedClasses: [String: [JavaClass]] = [:], expectedChunks: [String], file: StaticString = #filePath, @@ -653,13 +650,14 @@ func assertTranslatedClass( ) throws { let environment = try jvm.environment() let translator = JavaTranslator( + config: config, swiftModuleName: "SwiftModule", environment: environment, translateAsClass: asClass ) translator.translatedClasses = translatedClasses - translator.translatedClasses[javaType.fullJavaClassName] = (swiftTypeName, nil) + translator.translatedClasses[javaType.fullJavaClassName] = SwiftTypeName(module: nil, name: swiftTypeName) translator.nestedClasses = nestedClasses translator.startNewFile() @@ -677,12 +675,22 @@ func assertTranslatedClass( \(translatedDecls.map { $0.description }.joined(separator: "\n")) """ + func normalizeWhitespace(_ text: String) -> String { + return text.components(separatedBy: .newlines) + .map { $0.trimmingCharacters(in: .whitespaces) } + .joined(separator: "\n") + } + + let normalizedSwiftFileText = normalizeWhitespace(swiftFileText) + for expectedChunk in expectedChunks { - if swiftFileText.contains(expectedChunk) { + let normalizedExpectedChunk = normalizeWhitespace(expectedChunk) + + if normalizedSwiftFileText.contains(normalizedExpectedChunk) { continue } - XCTFail("Expected chunk '\(expectedChunk)' not found in '\(swiftFileText)'", file: file, line: line) + XCTFail("Expected chunk:\n---\n\(expectedChunk.yellow)\n---\nnot found in:\n===\n\(swiftFileText)\n===", file: file, line: line) } } } diff --git a/Tests/SwiftJavaToolLibTests/JavaTranslatorTests.swift b/Tests/SwiftJavaToolLibTests/JavaTranslatorTests.swift new file mode 100644 index 000000000..9983813d7 --- /dev/null +++ b/Tests/SwiftJavaToolLibTests/JavaTranslatorTests.swift @@ -0,0 +1,58 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JavaUtilJar +@_spi(Testing) import SwiftJava +import SwiftJavaConfigurationShared +import SwiftJavaShared +import SwiftJavaToolLib +import XCTest // NOTE: Workaround for https://github.com/swiftlang/swift-java/issues/43 +import _Subprocess + +class JavaTranslatorTests: XCTestCase { + + func translateGenericMethodParameters() async throws { + let classpathURL = try await compileJava( + """ + package com.example; + + class Item { + final T value; + Item(T item) { + this.value = item; + } + } + class Pair { } + + class ExampleSimpleClass { + Pair getPair( + String name, + Item key, + Item value + ) { return null; } + } + """) + + try withJavaTranslator( + javaClassNames: [ + "com.example.Item", + "com.example.Pair", + "com.example.ExampleSimpleClass", + ], + classpath: [classpathURL], + ) { translator in + + } + } +} diff --git a/Tests/SwiftJavaTests/JavaTranslatorValidationTests.swift b/Tests/SwiftJavaToolLibTests/JavaTranslatorValidationTests.swift similarity index 66% rename from Tests/SwiftJavaTests/JavaTranslatorValidationTests.swift rename to Tests/SwiftJavaToolLibTests/JavaTranslatorValidationTests.swift index a203486af..558add20d 100644 --- a/Tests/SwiftJavaTests/JavaTranslatorValidationTests.swift +++ b/Tests/SwiftJavaToolLibTests/JavaTranslatorValidationTests.swift @@ -12,17 +12,18 @@ // //===----------------------------------------------------------------------===// -import SwiftJavaLib +import SwiftJavaToolLib import XCTest +import SwiftJavaConfigurationShared final class JavaTranslatorValidationTests: XCTestCase { func testValidationError() throws { - let translator = try JavaTranslator(swiftModuleName: "SwiftModule", environment: jvm.environment()) + let translator = try JavaTranslator(config: Configuration(), swiftModuleName: "SwiftModule", environment: jvm.environment()) translator.translatedClasses = [ - "TestClass": ("Class1", "Module1"), - "TestClass2": ("Class1", "Module2"), - "TestClass3": ("Class1", "Module1"), - "TestClass4": ("Class1", nil) + "TestClass": SwiftTypeName(module: "Module1", name: "Class1"), + "TestClass2": SwiftTypeName(module: "Module2", name: "Class1"), + "TestClass3": SwiftTypeName(module: "Module1", name: "Class1"), + "TestClass4": SwiftTypeName(module: nil, name: "Class1") ] XCTAssertThrowsError(try translator.validateClassConfiguration()) { error in @@ -31,7 +32,7 @@ final class JavaTranslatorValidationTests: XCTestCase { switch validationError { case .multipleClassesMappedToSameName(let swiftToJavaMapping): XCTAssertEqual(swiftToJavaMapping, [ - JavaTranslator.SwiftToJavaMapping(swiftType: .init(swiftType: "Class1", swiftModule: "Module1"), + JavaTranslator.SwiftToJavaMapping(swiftType: .init(module: "Module1", name: "Class1"), javaTypes: ["TestClass", "TestClass3"]) ]) } diff --git a/Tests/SwiftJavaToolLibTests/TempFileTools.swift b/Tests/SwiftJavaToolLibTests/TempFileTools.swift new file mode 100644 index 000000000..5b133108d --- /dev/null +++ b/Tests/SwiftJavaToolLibTests/TempFileTools.swift @@ -0,0 +1,34 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024-2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +/// Example demonstrating how to create a temporary file using Swift Foundation APIs +public class TempFile { + + public static func create( + suffix: String, + _ contents: String = "", + in tempDirectory: URL = FileManager.default.temporaryDirectory) throws -> URL { + let tempFileName = "tmp_\(UUID().uuidString).\(suffix)" + let tempFileURL = tempDirectory.appendingPathComponent(tempFileName) + + try contents.write(to: tempFileURL, atomically: true, encoding: .utf8) + + return tempFileURL + } + public static func delete(at fileURL: URL) throws { + try FileManager.default.removeItem(at: fileURL) + } +} \ No newline at end of file diff --git a/Tests/SwiftJavaToolLibTests/WrapJavaTests/BasicWrapJavaTests.swift b/Tests/SwiftJavaToolLibTests/WrapJavaTests/BasicWrapJavaTests.swift new file mode 100644 index 000000000..74ed3ce28 --- /dev/null +++ b/Tests/SwiftJavaToolLibTests/WrapJavaTests/BasicWrapJavaTests.swift @@ -0,0 +1,91 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@_spi(Testing) import SwiftJava +import SwiftJavaToolLib +import JavaUtilJar +import SwiftJavaShared +import JavaNet +import SwiftJavaConfigurationShared +import _Subprocess +import XCTest // NOTE: Workaround for https://github.com/swiftlang/swift-java/issues/43 + +final class BasicWrapJavaTests: XCTestCase { + + func testWrapJavaFromCompiledJavaSource() async throws { + let classpathURL = try await compileJava( + """ + package com.example; + + class ExampleSimpleClass {} + """) + + try assertWrapJavaOutput( + javaClassNames: [ + "com.example.ExampleSimpleClass" + ], + classpath: [classpathURL], + expectedChunks: [ + """ + import CSwiftJavaJNI + import SwiftJava + """, + """ + @JavaClass("com.example.ExampleSimpleClass") + open class ExampleSimpleClass: JavaObject { + """ + ] + ) + } + + func test_wrapJava_doNotDupeImportNestedClassesFromSuperclassAutomatically() async throws { + let classpathURL = try await compileJava( + """ + package com.example; + + class SuperClass { + class SuperNested {} + } + + class ExampleSimpleClass { + class SimpleNested {} + } + """) + + try assertWrapJavaOutput( + javaClassNames: [ + "com.example.SuperClass", + "com.example.SuperClass$SuperNested", + "com.example.ExampleSimpleClass", + ], + classpath: [classpathURL], + expectedChunks: [ + """ + @JavaClass("com.example.SuperClass") + open class SuperClass: JavaObject { + """, + // FIXME: the mapping configuration could be used to nest this properly but today we don't automatically? + """ + @JavaClass("com.example.SuperClass$SuperNested") + open class SuperNested: JavaObject { + """, + """ + @JavaClass("com.example.SuperClass") + open class SuperClass: JavaObject { + """, + ] + ) + } + +} \ No newline at end of file diff --git a/Tests/SwiftJavaToolLibTests/WrapJavaTests/GenericsWrapJavaTests.swift b/Tests/SwiftJavaToolLibTests/WrapJavaTests/GenericsWrapJavaTests.swift new file mode 100644 index 000000000..ceb05df97 --- /dev/null +++ b/Tests/SwiftJavaToolLibTests/WrapJavaTests/GenericsWrapJavaTests.swift @@ -0,0 +1,412 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@_spi(Testing) import SwiftJava +import SwiftJavaToolLib +import JavaUtilJar +import SwiftJavaShared +import JavaNet +import SwiftJavaConfigurationShared +import _Subprocess +import XCTest // NOTE: Workaround for https://github.com/swiftlang/swift-java/issues/43 + +final class GenericsWrapJavaTests: XCTestCase { + + func testWrapJavaGenericMethod_singleGeneric() async throws { + let classpathURL = try await compileJava( + """ + package com.example; + + class Item { + final T value; + Item(T item) { + this.value = item; + } + } + class Pair { } + + class ExampleSimpleClass { + KeyType getGeneric(Item key) { return null; } + } + """) + + try assertWrapJavaOutput( + javaClassNames: [ + "com.example.Item", + "com.example.Pair", + "com.example.ExampleSimpleClass" + ], + classpath: [classpathURL], + expectedChunks: [ + """ + import CSwiftJavaJNI + import SwiftJava + """, + """ + @JavaClass("com.example.Pair") + open class Pair: JavaObject { + """, + """ + @JavaClass("com.example.ExampleSimpleClass") + open class ExampleSimpleClass: JavaObject { + """, + """ + @JavaMethod(typeErasedResult: "KeyType!") + open func getGeneric(_ arg0: Item?) -> KeyType! + """, + ] + ) + } + + // This is just a warning in Java, but a hard error in Swift, so we must 'prune' generic params + func testWrapJavaGenericMethod_pruneNotUsedGenericParam() async throws { + let classpathURL = try await compileJava( + """ + package com.example; + + class Item { + final T value; + Item(T item) { + this.value = item; + } + } + class Pair { } + + final class ExampleSimpleClass { + // use in return type + KeyType getGeneric() { + return null; + } + } + """) + + try assertWrapJavaOutput( + javaClassNames: [ + "com.example.Item", + "com.example.Pair", + "com.example.ExampleSimpleClass" + ], + classpath: [classpathURL], + expectedChunks: [ + """ + @JavaMethod(typeErasedResult: "KeyType!") + open func getGeneric() -> KeyType! + """, + ] + ) + } + + func testWrapJavaGenericMethod_multipleGenerics() async throws { + let classpathURL = try await compileJava( + """ + package com.example; + + class Item { + final T value; + Item(T item) { + this.value = item; + } + } + class Pair { } + + class ExampleSimpleClass { + Pair getPair(String name, Item key, Item value) { return null; } + } + """) + + try assertWrapJavaOutput( + javaClassNames: [ + "com.example.Item", + "com.example.Pair", + "com.example.ExampleSimpleClass" + ], + classpath: [classpathURL], + expectedChunks: [ + """ + import CSwiftJavaJNI + import SwiftJava + """, + """ + @JavaClass("com.example.Item") + open class Item: JavaObject { + """, + """ + @JavaClass("com.example.Pair") + open class Pair: JavaObject { + """, + """ + @JavaClass("com.example.ExampleSimpleClass") + open class ExampleSimpleClass: JavaObject { + """, + """ + @JavaMethod + open func getPair(_ arg0: String, _ arg1: Item?, _ arg2: Item?) -> Pair! + """, + ] + ) + } + + func test_Java2Swift_returnType_generic() async throws { + let classpathURL = try await compileJava( + """ + package com.example; + + // Mini decls in order to avoid warnings about some funcs we're not yet importing cleanly + final class List {} + final class Map {} + final class Number {} + + class GenericClass { + public T getClassGeneric() { return null; } + + public M getMethodGeneric() { return null; } + + public Map getMixedGeneric() { return null; } + + public String getNonGeneric() { return null; } + + public List getParameterizedClassGeneric() { return null; } + + public List getWildcard() { return null; } + + public T[] getGenericArray() { return null; } + } + """) + + try assertWrapJavaOutput( + javaClassNames: [ + "com.example.Map", + "com.example.List", + "com.example.Number", + "com.example.GenericClass", + ], + classpath: [classpathURL], + expectedChunks: [ + """ + @JavaMethod(typeErasedResult: "T!") + open func getClassGeneric() -> T! + """, + """ + @JavaMethod(typeErasedResult: "M!") + open func getMethodGeneric() -> M! + """, + """ + @JavaMethod + open func getMixedGeneric() -> Map! + """, + """ + @JavaMethod + open func getNonGeneric() -> String + """, + """ + @JavaMethod + open func getParameterizedClassGeneric() -> List! + """, + """ + @JavaMethod + open func getWildcard() -> List! + """, + """ + @JavaMethod + open func getGenericArray() -> [T?] + """, + ] + ) + } + + func testWrapJavaGenericSuperclass() async throws { + return // FIXME: we need this + + let classpathURL = try await compileJava( + """ + package com.example; + + class ByteArray {} + class CompressingStore extends AbstractStore {} + abstract class AbstractStore {} // implements Store {} + // interface Store {} + + """) + + try assertWrapJavaOutput( + javaClassNames: [ + "com.example.ByteArray", + // TODO: what if we visit in other order, does the wrap-java handle it + // "com.example.Store", + "com.example.AbstractStore", + "com.example.CompressingStore", + ], + classpath: [classpathURL], + expectedChunks: [ + """ + import CSwiftJavaJNI + import SwiftJava + """, + """ + @JavaClass("com.example.ByteArray") + open class ByteArray: JavaObject { + """, + // """ + // @JavaInterface("com.example.Store") + // public struct Store { + // """, + """ + @JavaClass("com.example.CompressingStore") + open class CompressingStore: AbstractStore { + """ + ] + ) + } + + func test_wrapJava_genericMethodTypeErasure_returnType() async throws { + let classpathURL = try await compileJava( + """ + package com.example; + + final class Kappa { + public T get() { return null; } + } + """) + + try assertWrapJavaOutput( + javaClassNames: [ + "com.example.Kappa", + ], + classpath: [classpathURL], + expectedChunks: [ + """ + import CSwiftJavaJNI + import SwiftJava + """, + """ + @JavaClass("com.example.Kappa") + open class Kappa: JavaObject { + @JavaMethod(typeErasedResult: "T!") + open func get() -> T! + } + """ + ] + ) + } + + func test_wrapJava_genericMethodTypeErasure_ofNullableOptional_staticMethods() async throws { + let classpathURL = try await compileJava( + """ + package com.example; + + final class Optional { + public static Optional ofNullable(T value) { return null; } + + public static T nonNull(T value) { return null; } + } + """) + + try assertWrapJavaOutput( + javaClassNames: [ + "com.example.Optional" + ], + classpath: [classpathURL], + expectedChunks: [ + """ + @JavaClass("com.example.Optional") + open class Optional: JavaObject { + """, + """ + extension JavaClass { + """, + """ + @JavaStaticMethod + public func ofNullable(_ arg0: T?) -> Optional! where ObjectType == Optional + } + """, + """ + @JavaStaticMethod(typeErasedResult: "T!") + public func nonNull(_ arg0: T?) -> T! where ObjectType == Optional + """ + ] + ) + } + + func test_wrapJava_genericMethodTypeErasure_customInterface_staticMethods() async throws { + let classpathURL = try await compileJava( + """ + package com.example; + + interface MyInterface {} + + final class Public { + public static void useInterface(T myInterface) { } + } + """) + + try assertWrapJavaOutput( + javaClassNames: [ + "com.example.MyInterface", + "com.example.Public" + ], + classpath: [classpathURL], + expectedChunks: [ + """ + @JavaInterface("com.example.MyInterface") + public struct MyInterface { + """, + """ + @JavaClass("com.example.Public") + open class Public: JavaObject { + """, + """ + extension JavaClass { + """, + """ + @JavaStaticMethod + public func useInterface(_ arg0: T?) + } + """ + ] + ) + } + + // TODO: this should be improved some more, we need to generated a `: Map` on the Swift side + func test_wrapJava_genericMethodTypeErasure_genericExtendsMap() async throws { + let classpathURL = try await compileJava( + """ + package com.example; + + final class Map {} + + final class Something { + public > M putIn(M map) { return null; } + } + """) + + try assertWrapJavaOutput( + javaClassNames: [ + "com.example.Map", + "com.example.Something", + ], + classpath: [classpathURL], + expectedChunks: [ + """ + @JavaClass("com.example.Something") + open class Something: JavaObject { + """, + """ + @JavaMethod(typeErasedResult: "M!") + open func putIn(_ arg0: M?) -> M! + """, + ] + ) + } + +} diff --git a/WIP.md b/WIP.md deleted file mode 100644 index e1f58612d..000000000 --- a/WIP.md +++ /dev/null @@ -1,34 +0,0 @@ -## Work In Progress - -This package is a work in progress, and many details are subject to change. - -Here is a long yet still very incomplete list of things we would like to do or -improve: - -- Expressivity gaps: - - [x] Translate Java exceptions into Swift and vice versa - - [ ] Expose Swift types to Java - - [x] Figure out when to promote from the local to the global heap - - [ ] Automatically turn get/set method pairs into Swift properties? - - [ ] Implement a global registry that lets us find the Swift type corresponding to a canonical Java class name (e.g., `java.net.URL` -> `JavaKitNetwork.URL`) - - [ ] Introduce overloads of `is` and `as` on the Swift projections so that conversion to any implemented interface or extended superclass returns non-optional. - - [ ] Figure out how to express the equivalent of `super.foo()` that calls the superclass's method from the subclass method. - - [ ] Recognize Java's enum classes and map them into Swift well - - [ ] Translate Java constants into Swift constants - - [ ] Support nested classes - - [x] Streamline the definition of "main" code in Swift - - [ ] Figure out how to subclass a Java class from Swift -- Tooling - - [ ] Extract documentation comments from Java and put them in the Swift projections - - [ ] [SwiftPM build plugin](https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/Plugins.md) to generate the Swift projections from Java as part of the build - - [x] Figure out how to launch the Java runtime from Swift code so we don't need to always start via `java` - - [x] Figure out how to unit-test this framework using Swift Testing - - [x] Add a "Jar mode" to `Java2Swift` that translates all classes in the given Jar file. - - [ ] Generate Swift projections for more common Java types into JavaKit libraries to make it easier to get started - - [ ] Teach `Java2Swift` when to create extensions of already-translated types that pick up any members that couldn't be translated because of missing types. See, for example, how `JavaKitReflection` adds extensions to `JavaClass` based on types like `Method` and `Parameter` -- Performance: - - [ ] Cache method/field IDs when we can - - [ ] Investigate noncopyable types to remove excess copies - - [ ] Investigate "unbridged" variants of String, Array, etc. - - [ ] Investigate the new [Foreign Function & Memory API](https://bugs.openjdk.org/browse/JDK-8312523) (aka Project Panama) for exposing Swift APIs to Java. - diff --git a/docker/Dockerfile b/docker/Dockerfile index c68ccc3c4..ef428b875 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,6 +1,6 @@ ARG swift_version=nightly-main ARG ubuntu_version=jammy -ARG base_image=docker.io/swiftlang/swift:$swift_version-$ubuntu_version +ARG base_image=docker.io/swiftlang/swift:${swift_version}-${ubuntu_version} FROM $base_image # needed to do again after FROM due to docker limitation ARG swift_version @@ -19,12 +19,12 @@ ENV LANG=en_US.UTF-8 ENV LANGUAGE=en_US.UTF-8 # JDK dependency -COPY install_jdk.sh . -RUN bash -xc 'JDK_VENDOR=Corretto ./install_jdk.sh' -ENV JAVA_HOME="/usr/lib/jvm/default-jdk" -ENV PATH="$PATH:/usr/lib/jvm/default-jdk/bin" +RUN curl -s "https://get.sdkman.io" | bash +RUN bash -c "source /root/.sdkman/bin/sdkman-init.sh && sdk install java 25.0.1-amzn" +ENV JAVA_HOME="$(sdk home java current)" -# Install "untested" nightly 'main' Swift -# TODO: Only do this if the released Swift is older than what we require -#COPY install_untested_nightly_swift.sh . -RUN #bash -xc './install_untested_nightly_swift.sh' +RUN curl -O https://download.swift.org/swiftly/linux/swiftly-$(uname -m).tar.gz && \ + tar zxf swiftly-$(uname -m).tar.gz && \ + ./swiftly init --quiet-shell-followup --assume-yes && \ + . "${SWIFTLY_HOME_DIR:-$HOME/.local/share/swiftly}/env.sh" && \ + hash -r diff --git a/docker/install_jdk.sh b/docker/install_jdk.sh index 8aa295ce5..b69545f9c 100755 --- a/docker/install_jdk.sh +++ b/docker/install_jdk.sh @@ -14,70 +14,92 @@ ##===----------------------------------------------------------------------===## set -euo pipefail -# Supported JDKs: Corretto or OpenJDK +# We need JDK 25 because that's the supported version with latest FFM +# However, we also need JDK 23 at most because Gradle does not support 24. + +# Supported JDKs: corretto if [ "$JDK_VENDOR" = "" ]; then -declare -r JDK_VENDOR="Corretto" +declare -r JDK_VENDOR="corretto" fi -echo "Installing $JDK_VENDOR JDK..." -apt-get update && apt-get install -y wget +apt-get update && apt-get install -y wget tree echo "Download JDK for: $(uname -m)" -if [ "$JDK_VENDOR" = 'OpenJDK' ]; then - if [ "$(uname -m)" = 'aarch64' ]; then - declare -r JDK_URL="https://download.java.net/java/GA/jdk23/3c5b90190c68498b986a97f276efd28a/37/GPL/openjdk-23_linux-aarch64_bin.tar.gz" - declare -r EXPECT_JDK_SHA="076dcf7078cdf941951587bf92733abacf489a6570f1df97ee35945ffebec5b7" - else - declare -r JDK_URL="https://download.java.net/java/GA/jdk23/3c5b90190c68498b986a97f276efd28a/37/GPL/$JDK_NAME" - declare -r EXPECT_JDK_SHA="08fea92724127c6fa0f2e5ea0b07ff4951ccb1e2f22db3c21eebbd7347152a67" - fi +download_and_install_jdk() { + local jdk_version="$1" + local jdk_url="" + local expected_md5="" - wget -q -O jdk.tar.gz "$JDK_URL" + echo "Installing $JDK_VENDOR JDK (${jdk_version})..." - declare JDK_SHA # on separate lines due to: SC2155 (warning): Declare and assign separately to avoid masking return values. - JDK_SHA="$(sha256sum jdk.tar.gz | cut -d ' ' -f 1)" - if [ "$JDK_SHA" != "$EXPECT_JDK_SHA" ]; then - echo "Downloaded JDK SHA does not match expected!" - echo "Expected: $EXPECT_JDK_SHA" - echo " Was: $JDK_SHA" - exit 1; - else - echo "JDK SHA is correct."; - fi -elif [ "$JDK_VENDOR" = 'Corretto' ]; then - if [ "$(uname -m)" = 'aarch64' ]; then - declare -r JDK_URL="https://corretto.aws/downloads/latest/amazon-corretto-22-aarch64-linux-jdk.tar.gz" - declare -r EXPECT_JDK_MD5="1ebe5f5229bb18bc784a1e0f54d3fe39" - else - declare -r JDK_URL="https://corretto.aws/downloads/latest/amazon-corretto-22-x64-linux-jdk.tar.gz" - declare -r EXPECT_JDK_MD5="5bd7fe30eb063699a3b4db7a00455841" - fi + if [ "$JDK_VENDOR" = 'corretto' ]; then + if [ "$(uname -m)" = 'aarch64' ]; then + case "$jdk_version" in + "25") + jdk_url="https://corretto.aws/downloads/latest/amazon-corretto-25-aarch64-linux-jdk.tar.gz" + expected_md5="37588d5d2a24b26525b9c563ad65cc77" + ;; + *) + echo "Unsupported JDK version: '$jdk_version'" + exit 1 + ;; + esac + else + case "$jdk_version" in + "25") + jdk_url="https://corretto.aws/downloads/latest/amazon-corretto-25-x64-linux-jdk.tar.gz" + expected_md5="7e56b1a9d71637ce4dc4047b23d0453e" + ;; + *) + echo "Unsupported JDK version: '$jdk_version'" + exit 1 + ;; + esac + fi + else + echo "Unsupported JDK vendor: '$JDK_VENDOR'" + exit 1 + fi - wget -q -O jdk.tar.gz "$JDK_URL" + # Download JDK + local jdk_filename="jdk_${jdk_version}.tar.gz" + wget -q -O "$jdk_filename" "$jdk_url" - declare JDK_MD5 # on separate lines due to: SC2155 (warning): Declare and assign separately to avoid masking return values. - JDK_MD5="$(md5sum jdk.tar.gz | cut -d ' ' -f 1)" - if [ "$JDK_MD5" != "$EXPECT_JDK_MD5" ]; then - echo "Downloaded JDK MD5 does not match expected!" - echo "Expected: $EXPECT_JDK_MD5" - echo " Was: $JDK_MD5" - exit 1; - else - echo "JDK MD5 is correct."; - fi -else - echo "Unsupported JDK vendor: '$JDK_VENDOR'" - exit 1 -fi + # Verify MD5 + local jdk_md5 + jdk_md5="$(md5sum "$jdk_filename" | cut -d ' ' -f 1)" + if [ "$jdk_md5" != "$expected_md5" ]; then + echo "Downloaded JDK $jdk_version MD5 does not match expected!" + echo "Expected: $expected_md5" + echo " Was: $jdk_md5" + exit 1 + else + echo "JDK $jdk_version MD5 is correct." + fi -# Extract and verify the JDK installation + # Extract and install JDK + mkdir -p "/usr/lib/jvm/jdk-${jdk_version}" + mv "$jdk_filename" "/usr/lib/jvm/jdk-${jdk_version}/" + cd "/usr/lib/jvm/jdk-${jdk_version}/" || exit 1 + tar xzf "$jdk_filename" && rm "$jdk_filename" -mkdir -p /usr/lib/jvm/ -mv jdk.tar.gz /usr/lib/jvm/ -cd /usr/lib/jvm/ -tar xzvf jdk.tar.gz && rm jdk.tar.gz -mv "$(find . -depth -maxdepth 1 -type d | head -n1)" default-jdk + # Move extracted directory to a standard name + local extracted_dir + extracted_dir="$(find . -maxdepth 1 -type d -name '*linux*' | head -n1)" + echo "move $extracted_dir to $(pwd)..." + mv "${extracted_dir}"/* . + echo "JDK $jdk_version installed successfully in /usr/lib/jvm/jdk-${jdk_version}/" + cd "$HOME" +} + +# Usage: Install JDK 25 +download_and_install_jdk "25" + +ls -la /usr/lib/jvm/ +cd /usr/lib/jvm/ +ln -s jdk-25 default-jdk +find . | grep java | grep bin echo "JAVA_HOME = /usr/lib/jvm/default-jdk" /usr/lib/jvm/default-jdk/bin/java -version \ No newline at end of file diff --git a/docker/install_untested_nightly_swift.sh b/docker/install_untested_nightly_swift.sh deleted file mode 100755 index 9cb920ab7..000000000 --- a/docker/install_untested_nightly_swift.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/bash -##===----------------------------------------------------------------------===## -## -## This source file is part of the Swift.org open source project -## -## Copyright (c) 2024 Apple Inc. and the Swift.org project authors -## Licensed under Apache License v2.0 -## -## See LICENSE.txt for license information -## See CONTRIBUTORS.txt for the list of Swift.org project authors -## -## SPDX-License-Identifier: Apache-2.0 -## -##===----------------------------------------------------------------------===## -set -euo pipefail - -echo "Download [nightly] [untested] Swift toolchain for: $(uname -m)" - -ARCH="$(arch)" -if [[ "$ARCH" = "i386" || "$ARCH" = "x86_64" ]]; then - SWIFT_UNTESTED_TOOLCHAIN_JOB_URL="https://ci.swift.org/job/oss-swift-package-ubuntu-22_04/lastSuccessfulBuild/consoleText" -else - SWIFT_UNTESTED_TOOLCHAIN_JOB_URL="https://ci.swift.org/job/oss-swift-package-ubuntu-22_04-aarch64/lastSuccessfulBuild/consoleText" -fi - -if [[ "$(grep "22.04" /etc/lsb-release)" = "" ]]; then - echo "This script specifically only supports Ubuntu 22.04 due to nightly toolchain availability" - exit 1 -fi - -UNTESTED_TOOLCHAIN_URL=$(curl -s $SWIFT_UNTESTED_TOOLCHAIN_JOB_URL | grep 'Toolchain: ' | sed 's/Toolchain: //g') -UNTESTED_TOOLCHAIN_FILENAME=$"toolchain.tar.gz" - -echo "Download toolchain: $UNTESTED_TOOLCHAIN_URL" - -cd / -curl -s "$UNTESTED_TOOLCHAIN_URL" > "$UNTESTED_TOOLCHAIN_FILENAME" - -swift -version - -echo "Extract toolchain: $UNTESTED_TOOLCHAIN_FILENAME" -tar xzf "$UNTESTED_TOOLCHAIN_FILENAME" -swift -version diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 000000000..182db452b --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +org.gradle.java.installations.fromEnv=JAVA_HOME_25,JAVA_HOME_25_X64,JAVA_HOME_25_ARM64 \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index a4b76b953..1b33c55ba 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 9355b4155..2e1113280 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index f5feea6d6..23d15a936 100755 --- a/gradlew +++ b/gradlew @@ -86,8 +86,7 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s -' "$PWD" ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -115,7 +114,7 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar +CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. @@ -206,7 +205,7 @@ fi DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. @@ -214,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 9b42019c7..5eed7ee84 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -70,11 +70,11 @@ goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar +set CLASSPATH= @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell diff --git a/settings.gradle b/settings.gradle index fa0fa5bd5..9bf38ba51 100644 --- a/settings.gradle +++ b/settings.gradle @@ -18,7 +18,8 @@ pluginManagement { rootProject.name = "swift-java" -include "SwiftKit" +include "SwiftKitCore" +include "SwiftKitFFM" // Include sample apps -- you can run them via `gradle Name:run` new File(rootDir, "Samples").listFiles().each {