diff --git a/.fleet/run.json b/.fleet/run.json new file mode 100644 index 0000000..3eb96dc --- /dev/null +++ b/.fleet/run.json @@ -0,0 +1,6 @@ +{ + "configurations": [ + + + ] +} \ No newline at end of file diff --git a/.fleet/settings.json b/.fleet/settings.json new file mode 100644 index 0000000..e69de29 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..aa26146 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,39 @@ +# https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions +# Renaming ? Change the README badge. +name: Build +on: + push: + branches: + - main + pull_request: +jobs: + BASE_CHECKS: + name: Base Checks + runs-on: ubuntu-latest + env: + GHUB_USER: ${{ secrets.GHUB_USER }} + GHUB_PERSONAL_ACCESS_TOKEN: ${{ secrets.GHUB_PERSONAL_ACCESS_TOKEN }} + SIGNING_KEY: ${{ secrets.SIGNING_KEY }} + SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + java-version: 17 + distribution: temurin + cache: gradle + + - name: Check local deployment + run: ./gradlew build deployLocal + + - name: Enable KVM group perms + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - name: Run tests + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 29 + script: cd tests && ./gradlew connectedCheck \ No newline at end of file diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..742cfca --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,26 @@ +name: Deploy +on: + release: + types: [published] +jobs: + SONATYPE_UPLOAD: + name: Sonatype Upload + runs-on: ubuntu-latest + env: + SIGNING_KEY: ${{ secrets.SIGNING_KEY }} + SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} + SONATYPE_USER: ${{ secrets.SONATYPE_USER }} + SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + GHUB_USER: ${{ secrets.GHUB_USER }} + GHUB_PERSONAL_ACCESS_TOKEN: ${{ secrets.GHUB_PERSONAL_ACCESS_TOKEN }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + java-version: 17 + distribution: temurin + cache: gradle + - name: Publish to Sonatype + run: ./gradlew deploySonatype + - name: Publish to GitHub Packages + run: ./gradlew deployGithub diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml new file mode 100644 index 0000000..f2da171 --- /dev/null +++ b/.github/workflows/snapshot.yml @@ -0,0 +1,27 @@ +# https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions +# Renaming ? Change the README badge. +name: Snapshot +on: + push: + branches: + - main +jobs: + SNAPSHOT: + name: Publish Snapshot + runs-on: ubuntu-latest + env: + SIGNING_KEY: ${{ secrets.SIGNING_KEY }} + SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} + SONATYPE_USER: ${{ secrets.SONATYPE_USER }} + SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + GHUB_USER: ${{ secrets.GHUB_USER }} + GHUB_PERSONAL_ACCESS_TOKEN: ${{ secrets.GHUB_PERSONAL_ACCESS_TOKEN }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + java-version: 17 + distribution: temurin + cache: gradle + - name: Publish sonatype snapshot + run: ./gradlew deploySonatypeSnapshot \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fae668c --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +.kotlin diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..5900f51 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "dependencies/jdk17"] + path = dependencies/jdk17 + url = https://android.googlesource.com/platform/prebuilts/jdk/jdk17 diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/artifacts/knee_annotations_frontend_0_1_1_SNAPSHOT.xml b/.idea/artifacts/knee_annotations_frontend_0_1_1_SNAPSHOT.xml new file mode 100644 index 0000000..f45eba0 --- /dev/null +++ b/.idea/artifacts/knee_annotations_frontend_0_1_1_SNAPSHOT.xml @@ -0,0 +1,6 @@ + + + $PROJECT_DIR$/knee-annotations/build/libs + + + \ No newline at end of file diff --git a/.idea/artifacts/knee_annotations_frontend_0_2_0_SNAPSHOT.xml b/.idea/artifacts/knee_annotations_frontend_0_2_0_SNAPSHOT.xml new file mode 100644 index 0000000..734a412 --- /dev/null +++ b/.idea/artifacts/knee_annotations_frontend_0_2_0_SNAPSHOT.xml @@ -0,0 +1,6 @@ + + + $PROJECT_DIR$/knee-annotations/build/libs + + + \ No newline at end of file diff --git a/.idea/artifacts/knee_annotations_frontend_0_3_0_SNAPSHOT.xml b/.idea/artifacts/knee_annotations_frontend_0_3_0_SNAPSHOT.xml new file mode 100644 index 0000000..c453508 --- /dev/null +++ b/.idea/artifacts/knee_annotations_frontend_0_3_0_SNAPSHOT.xml @@ -0,0 +1,8 @@ + + + $PROJECT_DIR$/knee-annotations/build/libs + + + + + \ No newline at end of file diff --git a/.idea/artifacts/knee_annotations_jvm_0_1_0_SNAPSHOT.xml b/.idea/artifacts/knee_annotations_jvm_0_1_0_SNAPSHOT.xml new file mode 100644 index 0000000..cf47572 --- /dev/null +++ b/.idea/artifacts/knee_annotations_jvm_0_1_0_SNAPSHOT.xml @@ -0,0 +1,8 @@ + + + $PROJECT_DIR$/knee-annotations/build/libs + + + + + \ No newline at end of file diff --git a/.idea/artifacts/knee_annotations_jvm_0_1_1_SNAPSHOT.xml b/.idea/artifacts/knee_annotations_jvm_0_1_1_SNAPSHOT.xml new file mode 100644 index 0000000..5b92fb1 --- /dev/null +++ b/.idea/artifacts/knee_annotations_jvm_0_1_1_SNAPSHOT.xml @@ -0,0 +1,8 @@ + + + $PROJECT_DIR$/knee-annotations/build/libs + + + + + \ No newline at end of file diff --git a/.idea/artifacts/knee_runtime_frontend_0_1_0_SNAPSHOT.xml b/.idea/artifacts/knee_runtime_frontend_0_1_0_SNAPSHOT.xml new file mode 100644 index 0000000..0d0f96b --- /dev/null +++ b/.idea/artifacts/knee_runtime_frontend_0_1_0_SNAPSHOT.xml @@ -0,0 +1,8 @@ + + + $PROJECT_DIR$/knee-runtime/build/libs + + + + + \ No newline at end of file diff --git a/.idea/artifacts/knee_runtime_frontend_0_1_1_SNAPSHOT.xml b/.idea/artifacts/knee_runtime_frontend_0_1_1_SNAPSHOT.xml new file mode 100644 index 0000000..ba7ca7c --- /dev/null +++ b/.idea/artifacts/knee_runtime_frontend_0_1_1_SNAPSHOT.xml @@ -0,0 +1,6 @@ + + + $PROJECT_DIR$/knee-runtime/build/libs + + + \ No newline at end of file diff --git a/.idea/artifacts/knee_runtime_frontend_0_2_0_SNAPSHOT.xml b/.idea/artifacts/knee_runtime_frontend_0_2_0_SNAPSHOT.xml new file mode 100644 index 0000000..6f75cf3 --- /dev/null +++ b/.idea/artifacts/knee_runtime_frontend_0_2_0_SNAPSHOT.xml @@ -0,0 +1,6 @@ + + + $PROJECT_DIR$/knee-runtime/build/libs + + + \ No newline at end of file diff --git a/.idea/artifacts/knee_runtime_frontend_0_3_0_SNAPSHOT.xml b/.idea/artifacts/knee_runtime_frontend_0_3_0_SNAPSHOT.xml new file mode 100644 index 0000000..97b8760 --- /dev/null +++ b/.idea/artifacts/knee_runtime_frontend_0_3_0_SNAPSHOT.xml @@ -0,0 +1,8 @@ + + + $PROJECT_DIR$/knee-runtime/build/libs + + + + + \ No newline at end of file diff --git a/.idea/artifacts/knee_runtime_jvm_0_1_0_SNAPSHOT.xml b/.idea/artifacts/knee_runtime_jvm_0_1_0_SNAPSHOT.xml new file mode 100644 index 0000000..f2da70c --- /dev/null +++ b/.idea/artifacts/knee_runtime_jvm_0_1_0_SNAPSHOT.xml @@ -0,0 +1,6 @@ + + + $PROJECT_DIR$/knee-runtime/build/libs + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..fb7f4a8 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml new file mode 100644 index 0000000..9b2a8c7 --- /dev/null +++ b/.idea/deploymentTargetDropDown.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..1315808 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,23 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..0df3cc5 --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 0000000..6d0ee1c --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..593cc99 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/annotations_deployLocal.xml b/.idea/runConfigurations/annotations_deployLocal.xml new file mode 100644 index 0000000..b9af6b5 --- /dev/null +++ b/.idea/runConfigurations/annotations_deployLocal.xml @@ -0,0 +1,24 @@ + + + + + + + true + true + false + false + + + \ No newline at end of file diff --git a/.idea/runConfigurations/compiler_plugin_deployLocal.xml b/.idea/runConfigurations/compiler_plugin_deployLocal.xml new file mode 100644 index 0000000..7edb57c --- /dev/null +++ b/.idea/runConfigurations/compiler_plugin_deployLocal.xml @@ -0,0 +1,23 @@ + + + + + + + true + true + false + + + \ No newline at end of file diff --git a/.idea/runConfigurations/deployLocal.xml b/.idea/runConfigurations/deployLocal.xml new file mode 100644 index 0000000..ca2317c --- /dev/null +++ b/.idea/runConfigurations/deployLocal.xml @@ -0,0 +1,24 @@ + + + + + + + true + true + false + false + + + \ No newline at end of file diff --git a/.idea/runConfigurations/gradle_plugin_deployLocal.xml b/.idea/runConfigurations/gradle_plugin_deployLocal.xml new file mode 100644 index 0000000..fc5ba5c --- /dev/null +++ b/.idea/runConfigurations/gradle_plugin_deployLocal.xml @@ -0,0 +1,23 @@ + + + + + + + true + true + false + + + \ No newline at end of file diff --git a/.idea/runConfigurations/runtime_deployLocal.xml b/.idea/runConfigurations/runtime_deployLocal.xml new file mode 100644 index 0000000..1fba660 --- /dev/null +++ b/.idea/runConfigurations/runtime_deployLocal.xml @@ -0,0 +1,23 @@ + + + + + + + true + true + false + + + \ No newline at end of file diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml new file mode 100644 index 0000000..e96534f --- /dev/null +++ b/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..1a13b52 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..454abc3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2022 DeepMedia Srl + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6fe6f6b --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +[![Build Status](https://github.com/deepmedia/Knee/workflows/Build/badge.svg?event=push)](https://github.com/deepmedia/Knee/actions) +[![Release](https://img.shields.io/github/release/deepmedia/Knee.svg)](https://github.com/deepmedia/Knee/releases) +[![Issues](https://img.shields.io/github/issues-raw/deepmedia/MavenDeployer.svg)](https://github.com/deepmedia/Knee/issues) + +![Project logo](assets/logo_256.png) + +# 🦵 Knee 🦵 + +A Kotlin compiler plugin and companion runtime tools that provides seamless communication between Kotlin/Native +binaries and Kotlin/JVM, using a thin and efficient layer around the JNI interface. + +With Knee, you can write idiomatic Kotlin/Native code, annotate it and then invoke it transparently from JVM +as if they were running on the same environment. + +```kotlin +// settings.gradle.kts +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + } +} + +// build.gradle.kts +plugins { + id("io.deepmedia.tools.knee") version "1.0.0" +} +``` + +Please check out [the documentation](https://opensource.deepmedia.io/knee). \ No newline at end of file diff --git a/assets/logo_256.png b/assets/logo_256.png new file mode 100644 index 0000000..20056d8 Binary files /dev/null and b/assets/logo_256.png differ diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..d2239d3 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,61 @@ +import io.deepmedia.tools.deployer.DeployerExtension +import io.deepmedia.tools.deployer.impl.SonatypeAuth + +plugins { + kotlin("multiplatform") apply false + kotlin("jvm") apply false + kotlin("plugin.serialization") apply false + id("io.deepmedia.tools.deployer") apply false +} + +subprojects { + group = providers.gradleProperty("knee.group").get() + version = providers.gradleProperty("knee.version").get() + + // Publishing + plugins.withId("io.deepmedia.tools.deployer") { + extensions.configure { + verbose.set(true) + + projectInfo { + description.set("A Kotlin Compiler Plugin for seamless communication between Kotlin/Native and Kotlin/JVM.") + url.set("https://github.com/deepmedia/Knee") + scm.fromGithub("deepmedia", "Knee") + license(apache2) + developer("natario1", "mattia@deepmedia.io", "DeepMedia", "https://deepmedia.io") + } + + signing { + key.set(secret("SIGNING_KEY")) + password.set(secret("SIGNING_PASSWORD")) + } + + // use "deployLocal" to deploy to local maven repository + localSpec() + + // use "deploySonatype" to deploy to OSSRH / maven central + sonatypeSpec { + auth.user.set(secret("SONATYPE_USER")) + auth.password.set(secret("SONATYPE_PASSWORD")) + } + + // use "deploySonatypeSnapshot" to deploy to sonatype snapshots repo + sonatypeSpec("snapshot") { + auth.user.set(secret("SONATYPE_USER")) + auth.password.set(secret("SONATYPE_PASSWORD")) + repositoryUrl.set(ossrhSnapshots1) + release.version.set("latest-SNAPSHOT") + } + + // use "deployGithub" to deploy to github packages + githubSpec { + repository.set("MavenDeployer") + owner.set("deepmedia") + auth { + user.set(secret("GHUB_USER")) + token.set(secret("GHUB_PERSONAL_ACCESS_TOKEN")) + } + } + } + } +} \ No newline at end of file diff --git a/dependencies/jdk17 b/dependencies/jdk17 new file mode 160000 index 0000000..c377e52 --- /dev/null +++ b/dependencies/jdk17 @@ -0,0 +1 @@ +Subproject commit c377e524831b2a4996884db862a024aa1c830907 diff --git a/docs/concepts.mdx b/docs/concepts.mdx new file mode 100644 index 0000000..177f923 --- /dev/null +++ b/docs/concepts.mdx @@ -0,0 +1,39 @@ +--- +title: Concepts +--- + +# Concepts + +## Motivation + +Native and JVM binaries have historically communicated through the Java Native Interface, which is a bridge across two +different environments and runtimes. This imposes strict restrictions about what kind of data can be passed through +the interface and how, together with the need to write very tedious boilerplate communication code on both platforms. + +Additionally, when using Kotlin/Native, the developer is required to deal with verbose, low-level `kotlinx.cinterop` types +in order to pass and receive JNI data. But Kotlin also creates an opportunity to improve communication by using the same language on the two ends of +the bridge. + +Our aim with Knee is to leverage this fact and, with the power of Kotlin compiler plugins, provide a transparent, seamless +interface between the two environments so that all the conversion boilerplate - whenever is needed - is hidden from the +developer. + +## Design + +> **Note**: Support is currently limited to Android Native targets, where jni.h is imported by default. +> Adding other platforms should be straightforward though, and we welcome contributions on this. + +In source code and documentation, you may see the following terms representing the two ends of the bridge: +- **backend** refers to the Kotlin/Native side (code, environment, binaries) +- **frontend** refers to the Kotlin/JVM side (code, environment, binaries) + +Knee is designed around the use case where the vast majority of the logic lives in the backend module, +and the frontend is just a very thin wrapper around it. This way, developers can just **write once in the backend**. + +This is done via a compiler plugin and a companion runtime library, that together: + +- Analyze backend code, transforming it where needed and generating glue code and JNI boilerplate code +- Generate frontend source code as `.kt` files, including all the declarations that are supposed to pass through the bridge +- Provide runtime utilities to deal with JNI functions in general (e.g. `currentJavaVirtualMachine`) + + diff --git a/docs/configure.mdx b/docs/configure.mdx new file mode 100644 index 0000000..cc46357 --- /dev/null +++ b/docs/configure.mdx @@ -0,0 +1,59 @@ +--- +title: Configure +--- + +# Configuration + +Like installation, configuration of Knee settings is done via the Gradle Plugin and Gradle properties. +The plugin will install an extension named `knee` with the following options: + +## Verbosity + +```kotlin +knee { + verboseLogs.set(true) // default: false + verboseRuntime.set(true) // default: false + verboseSources.set(true) // default: false +} +``` + +These three options should be used for debugging. They may also be controlled via a property in `gradle.properties`, +using the syntax `io.deepmedia.knee.=`. + +- `verboseLogs`: if enabled, the Gradle plugin will print logs to the terminal. +- `verboseRuntime`: if enabled, the compiler plugin will inject `println()` calls at runtime for debugging. +- `verboseSources`: if enabled, the generated frontend source files will include comments and extra elements to understand which code generated them. + +## Source Sets + +> In multiplatform projects with both backend and frontend targets, +> you can ignore this and just use [automatic target connection](#target-connection). + +As described in [the concepts page](concepts), Knee analyzes your native code and generates JVM sources. +These sources must be consumed somehow (e.g. added to your source sets). + +To retrieve and configure the directory, use: + +```kotlin +knee { + generatedSourceDirectory.get() // default dir is build/knee/src + generatedSourceDirectory.set(layout.buildDirectory.map { it.dir("somethingElse") }) + + val targetSpecificDir = generatedSourceDirectory(myKotlinTarget) +} +``` + +Note that the `generatedSourceDirectory` is a root directory, with one subfolder per each `KotlinNativeTarget` +where Knee is applied. + +## Target Connection + +In Android Multiplatform projects, Knee will try to automatically bind backends with frontends: + +- Determine all `androidNative` targets +- Declare debug and release binaries for them +- Pack the binaries in a dedicated folder (`build/knee/bin`) respecting Android's `jniLibs` convention +- Add such folder to Android Gradle Plugin and link tasks accordingly +- Add `knee.generatedSourceDirectory` to Android Gradle Plugin source sets and link tasks accordingly + +This can be disabled by using `knee.connectTargets.set(true)`. diff --git a/docs/features/buffers.mdx b/docs/features/buffers.mdx new file mode 100644 index 0000000..b6dba0b --- /dev/null +++ b/docs/features/buffers.mdx @@ -0,0 +1,70 @@ +--- +title: Buffers +--- + +# Buffers + +## Definition + +When dealing with memory and buffers, you may at some point need to pass them through the Native/JVM interface +efficiently by reference, avoiding copies especially as their size grows. + +Knee provides a built-in solution for this problem based on `java.nio` direct buffers and their native counterparts +defined by the `knee-runtime` package. You can use: + +- A direct `java.nio.ByteBuffer` on the JVM and `io.deepmedia.tools.knee.runtime.buffer.ByteBuffer` on native; +- A direct `java.nio.DoubleBuffer` on the JVM and `io.deepmedia.tools.knee.runtime.buffer.DoubleBuffer` on native; +- A direct `java.nio.FloatBuffer` on the JVM and `io.deepmedia.tools.knee.runtime.buffer.FloatBuffer` on native; +- A direct `java.nio.IntBuffer` on the JVM and `io.deepmedia.tools.knee.runtime.buffer.IntBuffer` on native; +- A direct `java.nio.LongBuffer` on the JVM and `io.deepmedia.tools.knee.runtime.buffer.LongBuffer` on native. + +Whenever such buffers are used as parameters or return types for Knee functions, the runtime will convert between the two. + +### Memory leaks + +Native buffers are just thin wrappers around a `CPointer`. Should you choose to allocate such buffers on the native side +(see [examples](#examples)), you **must free them after use**, using `buffer.free()`. + +> Natively allocated buffers are usable on the JVM only until the `buffer.free()` call + +This requirement is lifted when buffers are allocated on the JVM side and passed down. In this case, buffers will +keep a strong reference to the `java.nio.ByteBuffer`, so memory will be reclaimed only when all buffers (on both sides!) +go out of scope and are garbage collected. + +## Examples + +##### Allocate on JVM, pass down + +```kotlin +// JVM +val buffer = java.nio.ByteBuffer.allocateDirect(1024) +fillBuffer(buffer) + +// Native +@Knee fun fillBuffer(buffer: io.deepmedia.tools.knee.runtime.buffer.ByteBuffer) { + check(buffer.size == 1024) + val rawPointer: CArrayPointer = buffer.ptr + // Fill rawPointer... +} +``` + +##### Allocate natively, pass up + +```kotlin +// JVM +useBuffer(1024) { buffer: java.nio.ByteBuffer -> + check(buffer.capacity == 1024) + // Use it... +} + +// Native +@Knee fun useBuffer(size: Int, block: (io.deepmedia.tools.knee.runtime.buffer.ByteBuffer) -> Unit) { + val environment = currentJavaVirtualMachine.env!! + val buffer = io.deepmedia.tools.knee.runtime.buffer.ByteBuffer(environment, size) + try { + block(buffer) + } finally { + buffer.free() + } +} +``` \ No newline at end of file diff --git a/docs/features/builtin-types.mdx b/docs/features/builtin-types.mdx new file mode 100644 index 0000000..f059d39 --- /dev/null +++ b/docs/features/builtin-types.mdx @@ -0,0 +1,66 @@ +--- +title: Built-in types +--- + +# Built-in types + +Whenever [callables](../callables) are declared, Knee compiler's task is being able to serialize and deserialize, +both on the backend and on the frontend: + +- function arguments, or the property type for setters +- the function return type, or the property type for getters + +By default, Knee provides built-in support for many commonly used types, and utilities to define others +(for example, [enums](../enums), [classes](../classes), [interfaces](../interfaces)) and even import external declarations. + +## Primitives + +Most "primitive" language types are automatically supported: + +- `Int` and `UInt` +- `Long` and `ULong` +- `Byte` and `UByte` +- `Float` +- `Double` +- `Boolean` +- `String` + +So the following example works out of the box: + +```kotlin +@Knee fun sumAndDescribe(arg1: Int, arg2: Float, arg3: ULong): String { + val result = arg1.toDouble() + arg2.toDouble() + arg3.toLong().toDouble() + return result.toString() +} +``` + +## Special return types + +`Unit` and `Nothing` are also supported when used as return types. + +## Nullable types + +For any type `T` - both built-ins and types annotated by the developer - Knee is also able to serialize their nullable version `T?`. +Note that for primitive values, this comes at the well known cost of **boxing**. In the following example: + +```kotlin +@Knee fun countOrNull(): Int? { + return if (list.isEmpty()) null else list.size +} +``` +The `Int?` return type will be passed as a `java.lang.Integer` / `jobject`, not a simple `jint`. + +## Collections + +For any type `T` - both built-ins and types annotated by the developer - Knee is also able to serialize some of the +collection types which use `T` as their element type. + +For example, since `Int` is serializable, Knee can also serialize: +- `IntArray` +- `List` +- `Set` + +As you may, the performance of these options is not the same because the `IntArray` signature avoids boxing. + +> In case of non-primitive values, `Array` will be used. That may still perform better than `List` or `Set`, +> although not dramatically better. diff --git a/docs/features/callables.mdx b/docs/features/callables.mdx new file mode 100644 index 0000000..afb404d --- /dev/null +++ b/docs/features/callables.mdx @@ -0,0 +1,62 @@ +--- +title: Callables +--- + +# Callables + +We refer to functions and properties as *callables*. When appropriately annotated, callables can be invoked from +either side of the JNI interface (frontend or backend), execute your code on the other side and return some value. + +## Functions + +For a function to be available on the JVM side, it must be annotated with the `@Knee` annotation. +We support top-level functions and functions nested in `@KneeClass` declarations, as you can learn in [classes](../classes). +Upward functions (called from K/N, implemented on the JVM) are also available through [interfaces](../interfaces). + +```kotlin +// Kotlin/Native +@Knee fun topLevelFunction(): Int { + return 42 +} + +// Kotlin/JVM +check(topLevelFunction() == 42) +``` + +If you wish to have a different JVM name, use the name parameter: + +```kotlin +// Kotlin/Native +@Knee(name = "prettyName") fun uglyName(): Unit = ... + +// Kotlin/JVM +prettyName() +``` + +## Properties + +For a property to be available on the JVM side, it must be annotated with the `@Knee` annotation. +We support top-level properties and properties nested in `@KneeClass` declarations, as you can learn in [classes](../classes). +Upward properties (called from K/N, implemented on the JVM) are also available through [interfaces](../interfaces). + +Both `var` and `val` properties are supported. + +```kotlin +// Kotlin/Native +@Knee val immutableProp: Int = 42 +@Knee var mutableProp: Int = 0 + +// Kotlin/JVM +mutableProp = immutableProp +check(mutableProp == immutableProp) +``` + +If you wish to have a different JVM name, use the name parameter: + +```kotlin +// Kotlin/Native +@Knee(name = "prettyName") val uglyName: Int get() = ... + +// Kotlin/JVM +val prettyValue = prettyName +``` diff --git a/docs/features/classes.mdx b/docs/features/classes.mdx new file mode 100644 index 0000000..6bc464f --- /dev/null +++ b/docs/features/classes.mdx @@ -0,0 +1,82 @@ +--- +title: Classes +--- + +# Classes + +## Annotating classes + +Whenever you declare a class, you can use the `@KneeClass` annotation to tell the +compiler that it should be processed. This has a few implications that are important to understand. + +```kotlin +@KneeClass class Item(@Knee val id: String) + +@KneeClass class Database @Knee constructor(path: String) { + private val directory = Directory(path) + + @Knee fun loadItems(): List { ... } +} +``` + +#### JVM wrappers + +When a class is marked as `@KneeClass`, the compiler generates source code for the JVM in which the same class exists, +but is **a wrapper** to the underlying native instance. Using the `Database` example above, the generated JVM class +may look something like this: + +```kotlin +class Database internal constructor(private val native: Long) { + constructor(path: String) : this(native = NativeDatabase_init(path)) + fun loadItems(): List = NativeDatabase_loadItems(native) + protected fun finalize() = NativeDatabase_deinit(native) +} +``` + +You can see that: + +- JVM's `Database` is just a wrapper around the native `Database` instance, holding onto its native address (a `Long`) +- When JVM's `Database` is garbage collected, the native instance is notified to avoid leaks + +> You can use `@KneeClass(name = "OtherName")` to modify the JVM wrapper name. + +#### Pass by reference + +It may be useful to know that `@KneeClass` objects are passed through the JNI interface using the above mentioned +`Long` address, by reference. This means that **no data is being copied**: the source of truth remains on the native side, +and JVM users can easily invoke its functions and use its properties thanks to Knee. + +#### JVM construction + +While **the source of truth of a class is always on the native side**, you can still let JVM users create new instances. +This must be done explicitly by annotating one or more of the class constructors with the `@Knee` annotation. + +```kotlin +@KneeClass class Post(@Knee val id: String) +@KneeClass class User @Knee constructor(@Knee val id: String) +``` + +In the example above, `Post` can't be instantiated from the JVM side, while `User` can. + +> Even if JVM users can create instances, that doesn't mean that data lives on the JVM side. Simply, the JVM class constructor +> will call the native class constructor under the hood, and store a reference to it. + +## Annotating members + +All callable members (functions, properties, constructors) of a class can be made available to the JVM side, but +they must be explicitly marked with the `@Knee` annotation as described in the [callables](../callables) documentation. + +```kotlin +@KneeClass class Car { + @Knee fun driveHome() { ... } + fun driveWork() { ... } +} +``` + +In the example above, only the `driveHome` function will be available on the JVM side. + +## Importing classes + +If you wish to annotate existing classes that you don't control, for example those coming from a different module, +you can technically use `@KneeClass` on type aliases. Unfortunately as of now, this functionality is very limited in that you +can't choose which declarations will be imported. \ No newline at end of file diff --git a/docs/features/enums.mdx b/docs/features/enums.mdx new file mode 100644 index 0000000..01e19b8 --- /dev/null +++ b/docs/features/enums.mdx @@ -0,0 +1,46 @@ +--- +title: Enums +--- + +# Enums + +## Annotating enums + +Enums can be easily serialized through their ordinal value. You can use the `@KneeEnum` annotation to tell the +compiler that: + +- this native enum is expected to be serialized, so a JVM clone must be generated +- the compiler must serialize and deserialize these types whenever they are part of a [callable](../callables) declaration, e.g. a function argument or return type + +In the following example: + +```kotlin +@KneeEnum enum class DayOfWeek { + Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday +} + +@Knee fun getCurrentDay(): DayOfWeek = ... +``` + +Your JVM code can retrieve the current day with `getCurrentDay()` and receive a valid `DayOfWeek` instance back. +If you wish to have a different JVM name, use the name parameter: + +```kotlin +// Kotlin/Native +@KneeEnum(name = "WeekDay") enum class DayOfWeek { ... } + +// Kotlin/JVM +val currentDay: WeekDay = getCurrentDay() +``` + +## Importing enums + +If you wish to annotate existing enums that you don't control, for example those coming from a different module, +note that you can use `@KneeEnum` on type aliases. For example: + +```kotlin +@KneeEnum typealias DeprecationLevel = kotlin.DeprecationLevel +@KneeEnum typealias BufferOverflow = kotlinx.coroutines.channels.BufferOverflow +``` + +If the declaration is not found on the frontend, a clone will be generated, otherwise the existing declaration will be used. \ No newline at end of file diff --git a/docs/features/exceptions.mdx b/docs/features/exceptions.mdx new file mode 100644 index 0000000..84bced8 --- /dev/null +++ b/docs/features/exceptions.mdx @@ -0,0 +1,76 @@ +--- +title: Exceptions +--- + +# Exceptions + +Whenever a `@Knee` [callable](../callables) throws, the exception is thrown on the other side of the bridge. + +```kotlin +// Kotlin/Native +@Knee fun throwSomething() { + error("Something went wrong") +} + +// Kotlin/JVM +val failure = runCatching { throwSomething() }.exceptionOrNull() +checkNotNull(failure) +check(failure.message == "Something went wrong") +``` + +## Transparency + +By default, exceptions are not serializable and can't pass the JNI bridge (a `jthrowable` is not a `Throwable`!). +However, Knee strives to represents exception in the most transparent way, by reconstructing them with appropriate type +and parameters. + +##### Message preservation + +Whenever possible, the exception `message` is preserved, as can be seen in the example above. + +##### Type preservation + +Some common types are preserved. For example, a `kotlin.coroutines.cancellation.CancellationException` in the backend +will be re-thrown as a `java.util.concurrent.CancellationException` in the frontend. + +##### Instance preservation + +In some occasions, especially when lambdas are involved, the same exception can cross the JNI interface twice. +Consider the following example: + +```kotlin +// Native +@KneeInterface typealias StringMapper = (String) -> String + +@Knee fun mapString(source: String, mapper: StringMapper): String { + return mapper(source) +} +``` + +The JVM consumer code may try to `mapString`, but throw an exception: + +```kotlin +mapString("Hello") { throw IllegalStateException("Something went wrong") } +``` + +In this scenario, the `IllegalStateException` is thrown from the JVM, rethrown on the K/N side when invoking `mapper`, +and finally rethrown on the JVM side when `mapString` returns. That exception will be **exactly the same instance**, +meaning that you could check they're the same with `===`. + +While this may seem useless, many commonly used functions (notably, `Flow.collectWhile`) rely on these +checks and can only work when such mechanism is in place. + +## Custom exceptions + +You may also use custom exceptions, as long as they were properly annotated as [classes](../classes). + +```kotlin +@KneeClass +class CustomException @Knee constructor(message: String) : RuntimeException(message) { + @Knee + override val message: String? get() = super.message +} +``` + +Whenever a `CustomException` is thrown inside a Knee invocation, the runtime serializes it and creates a copy +for the other side of the JNI interface! You can also annotate other functions or properties for them to be exposed to JVM. diff --git a/docs/features/index.mdx b/docs/features/index.mdx new file mode 100644 index 0000000..8aca719 --- /dev/null +++ b/docs/features/index.mdx @@ -0,0 +1,82 @@ +--- +title: Features +docs: + - callables + - suspend-functions + - exceptions + - builtin-types + - enums + - classes + - interfaces + - buffers +--- + +# Features + +As described in [concepts](../concepts), to use Knee you must annotate your Kotlin/Native +declarations and they'll be made available in Kotlin/JVM. As a general rule: + +- Use `@Knee` on callables functions or properties +- Use `@KneeClass`, `@KneeInterface`, `@KneeEnum` on types (or typealiases) + +This way, the following Kotlin/Native code: + +```kotlin +@KneeClass class User(val id: String) + +@KneeClass class LoggedOutException : RuntimeException() + +@KneeClass class Post @Knee constructor(@Knee val title: String, @Knee val author: User) + +@KneeInterface typealias PostSavedCallback = (Post) -> Unit + +@KneeClass class Database() { + + private val scope: CoroutineScope = ... + private val disk: Disk = ... + + @Knee suspend fun getCurrentUser(): User { + val user = disk.readCurrentUserSuspending() + return user ?: throw LoggedOutException() + } + + @Knee fun savePostAsync(post: Post, callback: PostSavedCallback) { + scope.launch { + disk.writePostSuspending(post) + callback(post) + } + } +} + +@Knee val AppDatabase: Database = ... +``` + +...can be seamlessly called from the JVM side - no boilerplate, no glue code, everything is handled for you: + +```kotlin +suspend fun createPost(title: String): Post { + val user = try { + AppDatabase.getCurrentUser() + } catch (e: LoggedOutException) { + TODO("Handle this") + } + return Post(title, author = user) +} + +fun savePost(post: Post) { + AppDatabase.savePostAsync(post) { savedPost -> + check(savedPost == post) + } +} +``` + +We list most features supported by the Knee compiler below: + +- [Callables](callables) (functions and properties) +- [Suspend functions](suspend-functions) and structured concurrency +- [Exceptions](exceptions) +- [Built-in](builtin-types) types (primitives, nullables, collections) +- [Enum](enums) types +- [Class](classes) types +- [Interface](interfaces) types, lambdas and generics +- [java.nio Buffers](buffers) types diff --git a/docs/features/interfaces.mdx b/docs/features/interfaces.mdx new file mode 100644 index 0000000..42f128f --- /dev/null +++ b/docs/features/interfaces.mdx @@ -0,0 +1,147 @@ +--- +title: Interfaces +--- + +# Interfaces + +## Annotating interfaces + +> We recommend reading the [classes](../classes) documentation first. + +Whenever you declare an interface, you can use the `@KneeInterface` annotation to tell the +compiler that it should be processed. + +```kotlin +@KneeClass class Image(val contents: String) + +@KneeInterface interface ImageUploadCallbacks { + fun imageUploadStarted(image: Image) + fun imageUploadCompleted(image: Image) +} +``` + +Since the interface is declared on the native side but not available on the JVM, a copy of the declaration +will be generated for the JVM sources. + +> You can use `@KneeInterface(name = "OtherName")` to modify the JVM name. + +## Two-way implementation + +Unlike [classes](../classes), where the implementation of members is done on the Kotlin Native side and the JVM instance +is just a wrapper around it, `@KneeInterface` interface allow implementation from both sides. This makes it a much more +powerful tool! You can do either of the following: + +- Implement the interface natively, and pass it to the JVM. You will receive a thin JVM wrapper around the native interface +- Implement the interface on the JVM, and pass it to Kotlin Native. You will receive a thin native wrapper around the JVM interface + +For example, the code below is perfectly fine: + + +```kotlin +// Kotlin/Native +@Knee fun uploadImage(image: Image, callbacks: ImageUploadCallbacks) { + // ... downward call +} +``` + +But you may also implement interfaces natively and expose them: + +```kotlin +// Kotlin/Native +@KneeInterface interface ImageFactory { + fun createImage(): Image +} + +@Knee val DefaultImageFactory: ImageFactory = object: ImageFactory { + override fun createImage(): Image = // ... upward call +} +``` + +With this setup, the JVM code could do: + +```kotlin +// Kotlin/JVM +val image: Image = DefaultImageFactory.createImage() // K/JVM calls a K/N interface +uploadImage(image, object : ImageUploadCallbacks { // K/JVM interface called by K/N + override fun imageUploadStarted(image: Image) { ... } + override fun imageUploadCompleted(image: Image) { ... } +}) +``` + +## Annotating members + +Annotating callable members (functions, properties) of an interface **is not needed**. By default, all declarations +that are part of the interface contract will be marked as exported as if you added the `@Knee` annotation. + +## Importing interfaces + +If you wish to annotate existing interfaces that you don't control, for example those coming from a different module, +note that you can use `@KneeInterface` on type aliases. For example: + +```kotlin +@KneeInterface typealias MyInterface = SomeExternalInterface +``` + +You can now use `MyInterface` as a value parameter or return type of Knee functions, and pass it both ways. + +### Lambdas + +The most common use-case for imported interfaces is lambdas. In the Kotlin language, lambdas and suspend lambdas +extend the types `FunctionN` and `SuspendFunctionN`, where N is the number of function arguments. + +Luckily, you don't have to refer to these types and can use the lambda syntax directly: + +```kotlin +@KneeInterface typealias ImageMerger = (Image, Image) -> Image +@KneeInterface typealias ImageFetcher = suspend (String) -> Image? + +@Knee suspend fun mergeImages(fetcher: ImageFetcher, id1: String, id2: String, merger: ImageMerger): Image { + val image1 = fetcher(id1) ?: error("Not found") + val image2 = fetcher(id2) ?: error("Not found") + return merger(image1, image2) +} +``` + +> It is recommended to keep lambda typealiases `private`. Typealiases won't be available on the JVM. + +### Generics + +You may have noticed at this point that the import syntax (`@KneeInterface typealias ...`) supports generics, +something which regular interfaces (`@KneeInterface interface ...`) don't. + +The ability to specialize interfaces is not restricted to external declarations. Just declare a typealias to your own interface: + +```kotlin +interface EntityCallback { + fun entityCreated(entity: T) + fun entityDeleted(entity: T) +} + +@KneeInterface typealias ImageCallback = EntityCallback +``` + +You can now use `EntityCallback` as a value parameter or return type of Knee functions. + +### Example: flows + +A notable example of interface imports and generics, is the ability to import kotlinx's `Flow`. + +```kotlin +// Kotlin/Native +@KneeInterface private typealias ImagesFlow = Flow> +@KneeInterface private typealias ImagesFlowCollector = FlowCollector> + +@Knee fun loadImages(): Flow>> = ... + +// Kotlin/JVM +suspend fun loadImage(id: String): Flow { + return loadImages().map { list -> + list.firstOrNull { image -> image.id == id } + } +} +``` + +Note that since `Flow` refers to `FlowCollector`, we must also manually import the collector as well. + +You may use the same strategy to import `StateFlow`, `SharedFlow`, their `Mutable*` version, or really any other +interface that you can think of, as long as all types are correctly imported. \ No newline at end of file diff --git a/docs/features/suspend-functions.mdx b/docs/features/suspend-functions.mdx new file mode 100644 index 0000000..c2a99bf --- /dev/null +++ b/docs/features/suspend-functions.mdx @@ -0,0 +1,46 @@ +--- +title: Suspend functions +--- + +# Suspend Functions + +## Declaration + +All [functions](../callables#functions) that support the `@Knee` annotation can also be marked as `suspend`. +The developer UX is exactly the same: + +```kotlin +// Kotlin/Native +@Knee suspend fun computeNumber(): Int = coroutineScope { + val num1 = async { loadFirstNumber() } + val num2 = async { loadSecondNumber() } + num1.await() + num2.await() +} + +// Kotlin/JVM +scope.launch { + val number = computeNumber() + println("Found number: $number") +} +``` + +## Structured concurrency + +The underlying implementation is very complex in order to support two-way cancellation and error propagation. +In the example above: + +- If the JVM `scope` is cancelled, the native coroutines are also cancelled +- If the native coroutines are cancelled, `computeNumber` throws a `CancellationException` +- Errors are propagated up/down and the exception type, if possible, is [preserved](../exceptions) + +In short, calling a `@Knee` suspend function is no different than calling a local suspend function +and you can expect the same level of support. In particular Knee preserves the hierarchy of coroutines +and keeps them connected across the JNI bridge. + +##### Context elements + +Knee does no attempt at preserving the `CoroutineContext`. All context element, most notably the `CoroutineDispatcher`, +will be lost when the JNI bridge is crossed: + +- `@Knee` suspend functions called from K/JVM are invoked on `Dispatchers.Unconfined` on the native backend +- `@Knee` suspend functions called from K/N are invoked on `Dispatchers.Unconfined` on the JVM frontend \ No newline at end of file diff --git a/docs/index.mdx b/docs/index.mdx new file mode 100644 index 0000000..212412d --- /dev/null +++ b/docs/index.mdx @@ -0,0 +1,31 @@ +--- +title: Intro +docs: + - install + - concepts + - configure + - initialize + - features + - utilities +--- + +# Intro + +Knee is a Kotlin compiler plugin and companion runtime tools that provides seamless communication between Kotlin/Native +binaries and Kotlin/JVM, using a thin and efficient layer around the JNI interface. + +With Knee, you can write idiomatic Kotlin/Native code, annotate it and then invoke it transparently from JVM +as if they were running on the same environment. + +For a brief overview of Knee's capabilities and to see sample code, we recommend checking the [features](features) page +where you'll learn about all supported features such as: + +- Ability to call [functions](features/callables#functions), get or set [properties](features/callables#properties) across JNI +- [Suspend functions](features/suspend-functions) with two-way cancellation, holding structured concurrency together +- [Exception support](features/exceptions), including custom exception types +- Built-in serialization of [language primitives](features/builtin-types#primitives): numbers, strings, nullables, `Unit`, `Nothing` +- Built-in serialization of [collection types](features/builtin-types#collections): lists, sets, efficient arrays +- Custom [enums](features/enums) and [classes](features/classes) +- Custom [interfaces](features/interfaces) for two-way invocations +- Lambdas and [generics](features/interfaces#importing-interfaces) support +- [No-copy buffers](features/buffers), mapping `java.nio` buffers to `CPointer` on native diff --git a/docs/initialize.mdx b/docs/initialize.mdx new file mode 100644 index 0000000..8a470f5 --- /dev/null +++ b/docs/initialize.mdx @@ -0,0 +1,151 @@ +--- +title: Initialize +--- + +# Initialization + +Knee ships with a native runtime that deals with type conversions and other sorts of boilerplate logic, +while also providing some nice [utilities](utilities) for low-level JNI invocations. + +## Init calls + +To do so, the runtime must be initialized with a `JniEnvironment` (a `CPointer`) as soon as possible +in the application lifecycle. Any Knee-related calls that happen before initialization will likely lead to a crash. + +```kotlin +val environment: CPointer = ... +initKnee(environment) +``` + +Such a pointer can be retrieved in multiple ways. + +##### Using JNI_OnLoad + +The `JNI_OnLoad` function is called when the binary is loaded by the JVM using `System.loadLibrary`. A reference to +the JVM is passed down as well, and it can provide an environment for Knee. + +```kotlin +@CName(externName = "JNI_OnLoad") +fun onLoad(vm: JavaVirtualMachine): Int { + vm.useEnv { io.deepmedia.tools.knee.runtime.initKnee(it) } + return 0x00010006 // JNI_VERSION_1_6 +} +``` + +> Use carefully: only one library can export the `JNI_OnLoad` symbol. If you are developing a library to be consumed by others, +> this strategy is not recommended as they may have their own `JNI_OnLoad`. Prefer other strategies or [modules](#modules). + +##### Using JVM calls + +After the binary is loaded with `System.loadLibrary`, you can use any external function to invoke native code +and a `JniEnvironment` will be passed as well. That can be handed over to Knee: + +```kotlin +// Kotlin/JVM +package com.example +class KneeInitializer { + external fun initializeKnee() +} + +// Kotlin/Native +@CName(externName = "Java_com_example_KneeInitializer_initializeKnee") +fun initializeKnee(env: JniEnvironment) { + io.deepmedia.tools.knee.runtime.initKnee(env) +} +``` + +Note that this boilerplate (`external fun`, `@CName`...) is exactly what Knee will solve for all your other calls. + +## Modules + +All Kotlin Modules (e.g. Gradle projects) using Knee must be initialized. Knee supports module hierarchies in +two different ways. You can pick the one you find more appropriate to your codebase. + +##### Initialize in every module + +The simplest, but verbose, way of initializing all modules is to simply call `initKnee` separately in all of them. +This means that every module should add one [initialization call](#init-calls) and deal with Knee internally. + +```kotlin +// module A, at some point... +io.deepmedia.tools.knee.runtime.initKnee(env) + +// module B, at some point... +io.deepmedia.tools.knee.runtime.initKnee(env) +``` + +As long as the underlying JavaVM is the same, the runtime will be able to progressively load all modules this way, +meaning that the init call from a given module won't interfere with the one from other modules. + +##### Declare a KneeModule + +For more flexibility, libraries can avoid calling `initKnee` and declare a public object extending `io.deepmedia.tools.knee.runtime.module.KneeModule`. +Then, the consumer module can add a module dependency in their init call or in their module definition. + +For example, we may have a root module, `Lib1`, an intermediate module `Lib2` depending on `Lib1`, and the application module `App`. +In `Lib1`, simply declare a module: + +```kotlin +object Lib1Module : KneeModule() +``` + +In `Lib2`, again, declare a module, but declare the `Lib1` dependency: + +```kotlin +object Lib2Module : KneeModule(Lib1Module) // vararg +``` + +In the `App` module, pass the dependency to the initialization call: + +```kotlin +initKnee(environment, Lib2Module) // vararg +``` + +This way, the initialization call will also initialize the whole graph of modules that were declared. +It is even possible for your library to receive an initialization callback: + +```kotlin +object LibModule : KneeModule({ + initialize { environment -> + // Perform initialization logic (e.g. cache a jclass) + } +}) +``` + +## Exporting types + +Another benefit of declaring a `KneeModule` in multi-module hierarchies is **type exporting**. You will learn in [features](features) +that Knee allows you to mark specific classes or interfaces with annotations like `@KneeClass`, `@KneeInterface`, `@KneeEnum`. + +The presence of that annotation allows that type to travel through the JNI interface (in both ways) seamlessly. + +Sometimes, when creating library modules (for example, `:Lib`), such types should also be used by dependent modules (for example, `:App`) +and are supposed to pass through *their* JNI interface. By default this creates an error, because `:App` does not know +how to serialize and deserialize a type declared in the dependency module `:Lib`! + +Knee allows you to mark such types as **exported** through the `KneeModule` builder: + +```kotlin +// :Lib module +object LibModule : KneeModule({ + export() + export() + export>() +}) +``` + +This way you'll be able to serialize and deserialize types like `SomeType` in the app module: + +```kotlin +// :App module + +fun initialize(env: JniEnvironment) { + initKnee(env, LibModule) +} + +@Knee +fun doSomething(someType: SomeType): SomeOtherType { + // this is now allowed! + ... +} +``` \ No newline at end of file diff --git a/docs/install.mdx b/docs/install.mdx new file mode 100644 index 0000000..2f2cfe7 --- /dev/null +++ b/docs/install.mdx @@ -0,0 +1,47 @@ +--- +title: Install +--- + +# Installation + +Knee can be installed into your project using a Gradle Plugin. +The plugin will take care of adding runtime dependencies, applying the Kotlin Compiler plugin and more. + +## Configuration + +To use `Knee` in your project, add the following lines: + +```kotlin +// settings.gradle.kts +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + } +} + +// build.gradle.kts +plugins { + id("io.deepmedia.tools.knee") version "1.0.0" +} +``` + +## Snapshots + +We regularly push development snapshots of the library at `https://s01.oss.sonatype.org/content/repositories/snapshots/` +on each push to main. To use snapshots, add the url as a maven repository and depend on `latest-SNAPSHOT`: + +```kotlin +// settings.gradle.kts +pluginManagement { + repositories { + gradlePluginPortal() + maven(url = "https://s01.oss.sonatype.org/content/repositories/snapshots/") + } +} + +// build.gradle.kts +plugins { + id("io.deepmedia.tools.knee") version "latest-SNAPSHOT" +} +``` \ No newline at end of file diff --git a/docs/utilities.mdx b/docs/utilities.mdx new file mode 100644 index 0000000..56cedb9 --- /dev/null +++ b/docs/utilities.mdx @@ -0,0 +1,64 @@ +--- +title: Utilities +--- + +# Utilities + +On top of providing [initialization](initialize) APIs, the `knee-runtime` package contains a thin layer of utilities +wrapping the [JNI APIs](https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html) in a way +that's more comfortable to Kotlin users. + +> The `knee-runtime` package is automatically added to your project by the Gradle Plugin. + +## Environment and Virtual Machine + +We define two handy typealiases for the common JNI entry points, `*JNIEnv` and `*JavaVM`: + +```kotlin +typealias JniEnvironment = CPointer +typealias JavaVirtualMachine = CPointer +``` + +At any point after Knee [initialization](initialize), you can: + +- fetch the machine with `val machine = io.deepmedia.tools.knee.runtime.currentJavaVirtualMachine` +- fetch the machine's environment with `val environment = machine.env`. + +Due to JNI design, the machine's environment will only be non-null if it was previously attached to the current thread. +You can attach and detach the environment using regular JNI APIs, or use the `useEnv { }` utility: + +```kotlin +val jvm = currentJavaVirtualMachine +jvm.useEnv { environment -> + // useEnv attaches the current thread, and detaches it later + // unless an environment was already available, in which case it does no attach/detach +} +``` + +## API wrappers + +Most JNI APIs are available as extension functions to `JniEnvironment` and `JavaVirtualMachine`. +These function generally respect the original name and semantics, avoiding I/O conversions. For example: + +- `JniEnvironment.getBooleanField()` returns a `jboolean`, not a `Boolean` +- `JniEnvironment.newIntArray()` returns a `jintArray`, not an `IntArray` + +This design choice stems from the hope that, when using Knee, you shouldn't need to deal with JNI APIs at all, +so the wrappers can be more performant by avoiding opinionated conversions. + +The only exception to this rule is, well, **exceptions**: our wrappers automatically check return codes to be `JNI_OK` +and, where appropriate, invoke `ExceptionCheck`, `ExceptionOccurred`, `ExceptionClear`, and throw a `Throwable`. + +As an example, this is how, given an environment, you may create a JVM object and invoke a function on it. + +```kotlin +fun getClassFieldOrThrow(env: JniEnvironment): Long { + val objectClass: jclass = env.findClass("com/example/MyClass") + val objectConstructor: jmethodID = env.getMethodID(objectClass, "", "()V") + val objectInstance: jobject = env.newObject(objectClass, objectConstructor) + + val fieldMethod: jmethodID = env.getFieldId(objectClass, "myField", "J") + val fieldValue: Long = env.getLongField(objectInstance, fieldMethod) + return fieldValue +} +``` \ No newline at end of file diff --git a/experiments/.gitignore b/experiments/.gitignore new file mode 100644 index 0000000..603b140 --- /dev/null +++ b/experiments/.gitignore @@ -0,0 +1,14 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx diff --git a/experiments/.idea/.gitignore b/experiments/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/experiments/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/experiments/.idea/.name b/experiments/.idea/.name new file mode 100644 index 0000000..8506724 --- /dev/null +++ b/experiments/.idea/.name @@ -0,0 +1 @@ +KneeSamples \ No newline at end of file diff --git a/experiments/.idea/artifacts/knee_annotations_frontend_0_2_0_SNAPSHOT.xml b/experiments/.idea/artifacts/knee_annotations_frontend_0_2_0_SNAPSHOT.xml new file mode 100644 index 0000000..21cf822 --- /dev/null +++ b/experiments/.idea/artifacts/knee_annotations_frontend_0_2_0_SNAPSHOT.xml @@ -0,0 +1,6 @@ + + + $PROJECT_DIR$/../knee-annotations/build/libs + + + \ No newline at end of file diff --git a/experiments/.idea/artifacts/knee_runtime_frontend_0_2_0_SNAPSHOT.xml b/experiments/.idea/artifacts/knee_runtime_frontend_0_2_0_SNAPSHOT.xml new file mode 100644 index 0000000..9c11f1b --- /dev/null +++ b/experiments/.idea/artifacts/knee_runtime_frontend_0_2_0_SNAPSHOT.xml @@ -0,0 +1,6 @@ + + + $PROJECT_DIR$/../knee-runtime/build/libs + + + \ No newline at end of file diff --git a/experiments/.idea/compiler.xml b/experiments/.idea/compiler.xml new file mode 100644 index 0000000..fb7f4a8 --- /dev/null +++ b/experiments/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/experiments/.idea/gradle.xml b/experiments/.idea/gradle.xml new file mode 100644 index 0000000..929164c --- /dev/null +++ b/experiments/.idea/gradle.xml @@ -0,0 +1,42 @@ + + + + + + + \ No newline at end of file diff --git a/experiments/.idea/inspectionProfiles/Project_Default.xml b/experiments/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..103e00c --- /dev/null +++ b/experiments/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,32 @@ + + + + \ No newline at end of file diff --git a/experiments/.idea/kotlinc.xml b/experiments/.idea/kotlinc.xml new file mode 100644 index 0000000..f8467b4 --- /dev/null +++ b/experiments/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/experiments/.idea/misc.xml b/experiments/.idea/misc.xml new file mode 100644 index 0000000..cb73134 --- /dev/null +++ b/experiments/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/experiments/.idea/runConfigurations/compose_notes_c.xml b/experiments/.idea/runConfigurations/compose_notes_c.xml new file mode 100644 index 0000000..78d4727 --- /dev/null +++ b/experiments/.idea/runConfigurations/compose_notes_c.xml @@ -0,0 +1,23 @@ + + + + + + + true + true + false + + + \ No newline at end of file diff --git a/experiments/.idea/runConfigurations/compose_notes_l.xml b/experiments/.idea/runConfigurations/compose_notes_l.xml new file mode 100644 index 0000000..512aa00 --- /dev/null +++ b/experiments/.idea/runConfigurations/compose_notes_l.xml @@ -0,0 +1,23 @@ + + + + + + + true + true + false + + + \ No newline at end of file diff --git a/experiments/.idea/runConfigurations/expect_actual_c.xml b/experiments/.idea/runConfigurations/expect_actual_c.xml new file mode 100644 index 0000000..3798724 --- /dev/null +++ b/experiments/.idea/runConfigurations/expect_actual_c.xml @@ -0,0 +1,23 @@ + + + + + + + true + true + false + + + \ No newline at end of file diff --git a/experiments/.idea/runConfigurations/expect_actual_l.xml b/experiments/.idea/runConfigurations/expect_actual_l.xml new file mode 100644 index 0000000..5a2e60d --- /dev/null +++ b/experiments/.idea/runConfigurations/expect_actual_l.xml @@ -0,0 +1,23 @@ + + + + + + + true + true + false + + + \ No newline at end of file diff --git a/experiments/.idea/uiDesigner.xml b/experiments/.idea/uiDesigner.xml new file mode 100644 index 0000000..e96534f --- /dev/null +++ b/experiments/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/experiments/.idea/vcs.xml b/experiments/.idea/vcs.xml new file mode 100644 index 0000000..6c0b863 --- /dev/null +++ b/experiments/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/experiments/build.gradle.kts b/experiments/build.gradle.kts new file mode 100644 index 0000000..b7de2b2 --- /dev/null +++ b/experiments/build.gradle.kts @@ -0,0 +1,5 @@ +plugins { + kotlin("multiplatform") apply false + kotlin("jvm") apply false + id("com.android.application") apply false +} \ No newline at end of file diff --git a/experiments/compose-notes/.gitignore b/experiments/compose-notes/.gitignore new file mode 100644 index 0000000..f3d6549 --- /dev/null +++ b/experiments/compose-notes/.gitignore @@ -0,0 +1 @@ +/build/ \ No newline at end of file diff --git a/experiments/compose-notes/build.gradle.kts b/experiments/compose-notes/build.gradle.kts new file mode 100644 index 0000000..0001439 --- /dev/null +++ b/experiments/compose-notes/build.gradle.kts @@ -0,0 +1,108 @@ +import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget + +plugins { + kotlin("multiplatform") + id("com.android.application") + id("io.deepmedia.tools.knee") version "0.2.0-SNAPSHOT" +} + +configurations.configureEach { + resolutionStrategy.cacheChangingModulesFor(0, "seconds") +} + +android { + namespace = "io.deepmedia.tools.knee.sample.notes" + compileSdk = 33 + defaultConfig { + minSdk = 26 + targetSdk = 33 + } + sourceSets { + configureEach { + kotlin.srcDir("src/android${name.capitalize()}/kotlin") + manifest.srcFile("src/android${name.capitalize()}/AndroidManifest.xml") + } + } + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = "1.4.7" + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} + +knee { + enabled.set(true) + verbose.set(true) + autoBind.set(true) +} + +kotlin { + jvmToolchain(11) + + // frontend + androidTarget() + + // backend + val configureBackendTarget: KotlinNativeTarget.() -> Unit = { + fun KotlinCompilation<*>.configureBackendSourceSet() { + val sets = kotlin.sourceSets + val parent = sets.maybeCreate("backend${name.capitalize()}") + parent.dependsOn(sets["common${name.capitalize()}"]) + defaultSourceSet.dependsOn(parent) + } + compilations[KotlinCompilation.MAIN_COMPILATION_NAME].configureBackendSourceSet() + compilations[KotlinCompilation.TEST_COMPILATION_NAME].configureBackendSourceSet() + } + androidNativeArm32(configure = configureBackendTarget) + androidNativeArm64(configure = configureBackendTarget) + androidNativeX64(configure = configureBackendTarget) + androidNativeX86(configure = configureBackendTarget) +} + +dependencies { + implementation("androidx.core:core-ktx:1.9.0") + implementation("androidx.activity:activity-compose:1.5.1") + implementation("androidx.compose.material:material:1.2.1") + implementation("androidx.compose.animation:animation:1.2.1") + implementation("androidx.compose.ui:ui-tooling:1.2.1") +} + +/** + * For some reason, the android compose feature (enabled by buildFeatures.compose = true in the demo plugin) + * does not work with the KMP plugin, only with kotlin-android. There are a few tickets that were closed, + * like this for instance: https://issuetracker.google.com/issues/155536223 + * Workaround is to add the compose compiler plugin to the plugin classpath (could be done with freeCompilerArgs). + */ +/* dependencies { + val composeCompilerDependency = "androidx.compose.compiler:compiler:${android.composeOptions.kotlinCompilerExtensionVersion!!}" + configurations.configureEach { + if (name == "kotlinCompilerPluginClasspathAndroidDebug") add(name, composeCompilerDependency) + if (name == "kotlinCompilerPluginClasspathAndroidRelease") add(name, composeCompilerDependency) + } +}*/ + +val c by tasks.registering { + dependsOn("compileKotlinAndroidNativeArm32") +} + +val l by tasks.registering { + dependsOn("linkDebugSharedAndroidNativeArm32") +} + +/** + * This is to make included parent build work. Kotlin Compiler Plugins have a configuration + * issue: https://youtrack.jetbrains.com/issue/KT-53477/ . We workaround this in kotlin-compiler-plugin + * by using a fat JAR, but this fat JAR is exported from the "shadow" configuration, while included builds + * read from the "default" configuration and there doesn't seem to be a clean way to solve this. + */ +configurations.matching { + it.name.startsWith("kotlin") && it.name.contains("CompilerPluginClasspath") +}.all { + isTransitive = true +} \ No newline at end of file diff --git a/experiments/compose-notes/src/androidMain/AndroidManifest.xml b/experiments/compose-notes/src/androidMain/AndroidManifest.xml new file mode 100644 index 0000000..96d9a7a --- /dev/null +++ b/experiments/compose-notes/src/androidMain/AndroidManifest.xml @@ -0,0 +1,16 @@ + + + + + + + + + + \ No newline at end of file diff --git a/experiments/compose-notes/src/androidMain/kotlin/io/deepmedia/tools/knee/sample/DetailScreen.kt b/experiments/compose-notes/src/androidMain/kotlin/io/deepmedia/tools/knee/sample/DetailScreen.kt new file mode 100644 index 0000000..97186e8 --- /dev/null +++ b/experiments/compose-notes/src/androidMain/kotlin/io/deepmedia/tools/knee/sample/DetailScreen.kt @@ -0,0 +1,64 @@ +package io.deepmedia.tools.knee.sample + +import android.text.format.DateFormat +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Check +import androidx.compose.material.icons.rounded.Delete +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.unit.dp +import java.util.* + +@Composable +fun DetailScreen(noteManager: NoteManager, note: Note, modifier: Modifier = Modifier, navigate: (Destination) -> Unit) { + BackHandler { navigate(Destination.List) } + Scaffold( + modifier = modifier, + floatingActionButton = { + ExtendedFloatingActionButton( + text = { Text("Delete") }, + icon = { Image(Icons.Rounded.Delete, "Delete") }, + onClick = { + noteManager.removeNote(note.id) + navigate(Destination.List) + }, + ) + } + ) { padding -> + Column( + modifier = Modifier.fillMaxSize().padding(padding).verticalScroll(rememberScrollState()) + ) { + + Text( + text = "Note", + modifier = Modifier.padding(top = 96.dp, bottom = 16.dp).padding(horizontal = 24.dp), + style = MaterialTheme.typography.h4.copy(fontFamily = FontFamily.Monospace) + ) + + val context = LocalContext.current + val format = remember(context) { DateFormat.getMediumDateFormat(context) } + val date = remember(note.date) { format.format(Date(note.date)) } + Text( + text = "${note.author}, $date", + modifier = Modifier.padding(horizontal = 16.dp), + fontFamily = FontFamily.Monospace, + style = MaterialTheme.typography.subtitle2 + ) + Spacer(Modifier.height(16.dp)) + Text( + text = note.content, + modifier = Modifier.padding(horizontal = 16.dp), + ) + } + } +} \ No newline at end of file diff --git a/experiments/compose-notes/src/androidMain/kotlin/io/deepmedia/tools/knee/sample/EditorScreen.kt b/experiments/compose-notes/src/androidMain/kotlin/io/deepmedia/tools/knee/sample/EditorScreen.kt new file mode 100644 index 0000000..b784b6e --- /dev/null +++ b/experiments/compose-notes/src/androidMain/kotlin/io/deepmedia/tools/knee/sample/EditorScreen.kt @@ -0,0 +1,67 @@ +package io.deepmedia.tools.knee.sample + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Check +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.unit.dp +import java.util.* + +@Composable +fun EditorScreen(noteManager: NoteManager, modifier: Modifier = Modifier, navigate: (Destination) -> Unit) { + BackHandler { navigate(Destination.List) } + var author by remember { mutableStateOf("") } + var content by remember { mutableStateOf("") } + Scaffold( + modifier = modifier, + floatingActionButton = { + ExtendedFloatingActionButton( + text = { Text("Save") }, + icon = { Image(Icons.Rounded.Check, "Save") }, + onClick = { + if (author.isBlank() || content.isBlank()) return@ExtendedFloatingActionButton + val note = Note(UUID.randomUUID().toString(), author, System.currentTimeMillis(), content) + noteManager.addNote(note) + navigate(Destination.List) + }, + ) + } + ) { padding -> + Column(Modifier.fillMaxSize().padding(padding).verticalScroll(rememberScrollState())) { + Box( + Modifier.padding(top = 96.dp, bottom = 16.dp).padding(horizontal = 24.dp) + ) { + val style = MaterialTheme.typography.h4.copy( + fontFamily = FontFamily.Monospace + ) + if (author.isEmpty()) { + Text("Who?", Modifier.alpha(0.5F), style = style) + } + BasicTextField( + value = author, + onValueChange = { author = it.replace("\n", "") }, + textStyle = style, + maxLines = 1, + ) + } + Box(Modifier.padding(16.dp)) { + if (content.isEmpty()) { + Text("Write content...", Modifier.alpha(0.5F)) + } + BasicTextField(content, { content = it }) + } + } + } +} \ No newline at end of file diff --git a/experiments/compose-notes/src/androidMain/kotlin/io/deepmedia/tools/knee/sample/ListScreen.kt b/experiments/compose-notes/src/androidMain/kotlin/io/deepmedia/tools/knee/sample/ListScreen.kt new file mode 100644 index 0000000..2c15906 --- /dev/null +++ b/experiments/compose-notes/src/androidMain/kotlin/io/deepmedia/tools/knee/sample/ListScreen.kt @@ -0,0 +1,85 @@ +package io.deepmedia.tools.knee.sample + +import android.text.format.DateFormat +import androidx.activity.compose.setContent +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Add +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import java.util.Date + + +// Use NoteManager.size and NoteManager.noteAt to collect a list +// (until we add proper list/array support!) +// private val NoteManager.notes get() = Array(size) { noteAt(it) }.toList() +// EDIT: list support added +private val NoteManager.notes get() = current + + +@Composable +fun ListScreen(noteManager: NoteManager, modifier: Modifier = Modifier, navigate: (Destination) -> Unit) { + var notes by remember { mutableStateOf(noteManager.notes) } + DisposableEffect(noteManager) { + val callback = object : NoteManager.Callback { + override fun onNoteAdded(note: Note) { notes = noteManager.notes } + override fun onNoteRemoved(note: Note) { notes = noteManager.notes } + } + noteManager.registerCallback(callback) + onDispose { noteManager.unregisterCallback(callback) } + } + + Scaffold( + modifier = modifier, + floatingActionButton = { + FloatingActionButton( + onClick = { navigate(Destination.Editor) }, + content = { Image(imageVector = Icons.Rounded.Add, contentDescription = "Add") } + ) + } + ) { padding -> + LazyColumn(modifier = Modifier.fillMaxSize().padding(padding)) { + item { + Text( + text = "Notes", + fontFamily = FontFamily.Monospace, + style = MaterialTheme.typography.h4, + modifier = Modifier.padding(top = 96.dp, bottom = 16.dp).padding(horizontal = 24.dp) + ) + } + items(notes.reversed()) { note -> + NotePreview(note, Modifier.fillMaxWidth() + .clickable { navigate(Destination.Detail(note)) } + .padding(vertical = 8.dp, horizontal = 16.dp)) + } + } + } +} + +@Composable +private fun NotePreview(note: Note, modifier: Modifier = Modifier) { + val context = LocalContext.current + val format = remember(context) { DateFormat.getMediumDateFormat(context) } + val date = remember(note.date) { format.format(Date(note.date)) } + Column(modifier, verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text( + text = "${note.author}, $date", + fontFamily = FontFamily.Monospace, + style = MaterialTheme.typography.subtitle2 + ) + Text( + text = note.content, + maxLines = 4, + overflow = TextOverflow.Ellipsis + ) + } +} \ No newline at end of file diff --git a/experiments/compose-notes/src/androidMain/kotlin/io/deepmedia/tools/knee/sample/RootScreen.kt b/experiments/compose-notes/src/androidMain/kotlin/io/deepmedia/tools/knee/sample/RootScreen.kt new file mode 100644 index 0000000..9802325 --- /dev/null +++ b/experiments/compose-notes/src/androidMain/kotlin/io/deepmedia/tools/knee/sample/RootScreen.kt @@ -0,0 +1,48 @@ +package io.deepmedia.tools.knee.sample + +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.lightColors +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier + + +class NotesActivity : androidx.activity.ComponentActivity() { + companion object { + init { + System.loadLibrary("compose_notes") + } + } + + override fun onCreate(savedInstanceState: android.os.Bundle?) { + super.onCreate(savedInstanceState) + setContent { + MaterialTheme(colors = lightColors()) { + Surface { + RootScreen(Modifier.fillMaxSize()) + } + } + } + } +} + +sealed interface Destination { + object List : Destination + class Detail(val note: Note) : Destination + object Editor : Destination +} + +@Composable +fun RootScreen(modifier: Modifier = Modifier) { + val noteManager = remember { NoteManager() } + /* DisposableEffect(Unit) { onDispose { noteManager.finalize() } } */ + + var currentDestination by remember { mutableStateOf(Destination.List) } + when (val dest = currentDestination) { + is Destination.List -> ListScreen(noteManager, modifier) { currentDestination = it } + is Destination.Detail -> DetailScreen(noteManager, dest.note, modifier) { currentDestination = it } + is Destination.Editor -> EditorScreen(noteManager, modifier) { currentDestination = it } + } +} \ No newline at end of file diff --git a/experiments/compose-notes/src/backendMain/kotlin/Init.kt b/experiments/compose-notes/src/backendMain/kotlin/Init.kt new file mode 100644 index 0000000..1223399 --- /dev/null +++ b/experiments/compose-notes/src/backendMain/kotlin/Init.kt @@ -0,0 +1,12 @@ +package io.deepmedia.tools.knee.sample + +import io.deepmedia.tools.knee.annotations.* + +@OptIn(ExperimentalStdlibApi::class) +@CName(externName = "JNI_OnLoad") +@KneeInit +fun initKnee() { + require(isExperimentalMM()) { + "Not experimental MM" + } +} diff --git a/experiments/compose-notes/src/backendMain/kotlin/Note.kt b/experiments/compose-notes/src/backendMain/kotlin/Note.kt new file mode 100644 index 0000000..2cb923f --- /dev/null +++ b/experiments/compose-notes/src/backendMain/kotlin/Note.kt @@ -0,0 +1,57 @@ +package io.deepmedia.tools.knee.sample + +import io.deepmedia.tools.knee.annotations.* +import kotlinx.cinterop.UnsafeNumber +import kotlinx.cinterop.alloc +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.ptr +import platform.posix.gettimeofday +import platform.posix.timeval +import kotlin.random.Random + +@KneeClass +data class Note @Knee constructor( + @Knee val id: String, + @Knee val author: String, + @Knee val date: Long, + @Knee val content: String +) + +private val FakeAuthors = listOf("Kate", "Emma", "John", "Mark", "Lucy", "Richard", "Joe") + +private val FakeWords = """ + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum mattis massa non auctor sodales. Fusce mattis non erat quis euismod. Etiam suscipit enim sed luctus efficitur. Sed ultrices tincidunt maximus. Donec rutrum, dolor nec porta fringilla, arcu magna tincidunt dui, non sollicitudin est lectus id quam. Vestibulum sit amet suscipit diam. Sed et risus ut ex eleifend scelerisque facilisis sit amet metus. Morbi a erat mauris. Morbi et sem lacinia, sagittis eros et, sodales libero. Cras ac ligula in leo blandit scelerisque ac vitae nisl. Maecenas leo mi, fermentum nec ante sed, sagittis rutrum felis. + Morbi ipsum tortor, dictum iaculis nisi et, egestas lacinia orci. Cras congue est ante, semper dapibus nibh laoreet et. Duis et scelerisque eros. Sed in nisl sed nisi facilisis fringilla feugiat sed est. Sed quis tempus diam. Ut bibendum quam vel mi ultrices hendrerit. Donec fermentum rhoncus tellus. Fusce quis tellus a metus suscipit blandit non eget velit. Curabitur pharetra porttitor ipsum, eu elementum neque elementum ac. Nam accumsan augue lacus, ac tincidunt justo pretium porta. Integer rutrum enim feugiat purus venenatis, quis rutrum nulla tincidunt. Duis faucibus velit id lacus malesuada, nec bibendum elit interdum. Pellentesque id sem a sem tristique fringilla eget ut nibh. Pellentesque ultrices finibus nisl, non egestas quam semper mollis. Nullam ut libero velit. + Sed ultrices velit eu laoreet pharetra. Nulla nec ex sed elit sodales elementum. Nam rutrum ultrices ante vestibulum consequat. Pellentesque nibh quam, venenatis quis pharetra vitae, congue id enim. Vestibulum tellus nisl, aliquam id pellentesque suscipit, convallis sed ipsum. Quisque semper ut dui non maximus. Fusce eleifend neque vitae orci vulputate, eu viverra mi pellentesque. Suspendisse consequat purus in enim blandit congue. Duis imperdiet consectetur sapien a finibus. Duis aliquam pharetra rutrum. Phasellus mollis sit amet lorem sed vestibulum. + Phasellus pharetra lacus imperdiet ultricies imperdiet. Ut at dui urna. Aliquam vitae venenatis enim. In hac habitasse platea dictumst. Cras id sapien dui. Aliquam ut velit condimentum, imperdiet ex et, pharetra lacus. Donec ullamcorper risus ac nunc lobortis, sed finibus nisi iaculis. Maecenas sodales at tellus eget varius. Quisque neque arcu, auctor et consectetur sed, consectetur in enim. + Donec metus nunc, faucibus ac consectetur ut, viverra at lacus. Nunc mattis placerat elit, sit amet lobortis ipsum posuere a. Morbi vel interdum erat, sit amet efficitur nisi. Praesent ut massa ullamcorper, cursus sem quis, luctus orci. Pellentesque fermentum lobortis suscipit. Donec elementum mauris placerat sem porta, ac rhoncus quam lacinia. Maecenas ut augue id elit placerat tincidunt. Mauris aliquet purus massa, vitae accumsan lacus volutpat eu. Cras dignissim tempor purus vel ultricies. Vivamus imperdiet vitae felis nec dictum. Vestibulum posuere tortor sapien, eu elementum magna aliquam sit amet. Nam varius sed odio sed cursus. Curabitur non finibus lacus. Nullam eleifend faucibus tortor vitae cursus. Maecenas ornare lectus eu commodo venenatis. +""" + .replace('\n', ' ') + .filter { it.isLetter() || it.isWhitespace() } + .lowercase() + .split(' ') + .filter { it.isNotEmpty() } + +private fun fakeContent(words: Int = 200) = (0 until words) + .map { FakeWords.random() } + .joinToString(" ") + "." + +private fun fakeTime(): Long { + @OptIn(UnsafeNumber::class) + val nowMs = memScoped { + val timeval = alloc() + gettimeofday(timeval.ptr, null) + timeval.tv_sec * 1000L + } + val lastYearMs = nowMs - (1000L * 60 * 60 * 24 * 365) + return lastYearMs + (Random.nextFloat() * (nowMs - lastYearMs)).toLong() +} + +val FakeNotes = (0 until 10).map { + Note( + id = Random.nextLong().toString(), + author = FakeAuthors.random(), + date = fakeTime(), + content = fakeContent() + ) +} \ No newline at end of file diff --git a/experiments/compose-notes/src/backendMain/kotlin/NoteManager.kt b/experiments/compose-notes/src/backendMain/kotlin/NoteManager.kt new file mode 100644 index 0000000..6e86b66 --- /dev/null +++ b/experiments/compose-notes/src/backendMain/kotlin/NoteManager.kt @@ -0,0 +1,41 @@ +package io.deepmedia.tools.knee.sample + +import io.deepmedia.tools.knee.annotations.* + + +@KneeClass class NoteManager @Knee constructor() { + + private val notes = mutableListOf(*FakeNotes.sortedBy { it.date }.toTypedArray()) + private val callbacks = mutableListOf() + + @Knee fun addNote(note: Note) { + if (notes.add(note)) { + callbacks.forEach { it.onNoteAdded(note) } + } + } + + @Knee fun removeNote(id: String) { + notes.filter { it.id == id }.forEach { note -> + notes.remove(note) + callbacks.forEach { it.onNoteRemoved(note) } + } + } + + @Knee val current: List get() = notes + + @Knee val size get() = notes.size + + @Knee fun registerCallback(callback: Callback) { + callbacks.add(callback) + } + + @Knee fun unregisterCallback(callback: Callback) { + callbacks.remove(callback) + } + + @KneeInterface + interface Callback { + fun onNoteAdded(note: Note) + fun onNoteRemoved(note: Note) + } +} \ No newline at end of file diff --git a/experiments/expect-actual/.gitignore b/experiments/expect-actual/.gitignore new file mode 100644 index 0000000..f3d6549 --- /dev/null +++ b/experiments/expect-actual/.gitignore @@ -0,0 +1 @@ +/build/ \ No newline at end of file diff --git a/experiments/expect-actual/build.gradle.kts b/experiments/expect-actual/build.gradle.kts new file mode 100644 index 0000000..2ea52a2 --- /dev/null +++ b/experiments/expect-actual/build.gradle.kts @@ -0,0 +1,108 @@ +import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget + +plugins { + kotlin("multiplatform") + id("com.android.application") + id("io.deepmedia.tools.knee") version "0.2.0-SNAPSHOT" +} + +configurations.configureEach { + resolutionStrategy.cacheChangingModulesFor(0, "seconds") +} + +android { + namespace = "io.deepmedia.tools.knee.sample.expect" + compileSdk = 33 + defaultConfig { + minSdk = 26 + targetSdk = 33 + } + sourceSets { + configureEach { + kotlin.srcDir("src/android${name.capitalize()}/kotlin") + manifest.srcFile("src/android${name.capitalize()}/AndroidManifest.xml") + } + } + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = "1.4.7" + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} + +knee { + enabled.set(true) + verbose.set(true) + autoBind.set(true) +} + +kotlin { + jvmToolchain(11) + + // frontend + androidTarget() + + // backend + val configureBackendTarget: KotlinNativeTarget.() -> Unit = { + fun KotlinCompilation<*>.configureBackendSourceSet() { + val sets = kotlin.sourceSets + val parent = sets.maybeCreate("backend${name.capitalize()}") + parent.dependsOn(sets["common${name.capitalize()}"]) + defaultSourceSet.dependsOn(parent) + } + compilations[KotlinCompilation.MAIN_COMPILATION_NAME].configureBackendSourceSet() + compilations[KotlinCompilation.TEST_COMPILATION_NAME].configureBackendSourceSet() + } + androidNativeArm32(configure = configureBackendTarget) + androidNativeArm64(configure = configureBackendTarget) + androidNativeX64(configure = configureBackendTarget) + androidNativeX86(configure = configureBackendTarget) +} + +dependencies { + implementation("androidx.core:core-ktx:1.9.0") + implementation("androidx.activity:activity-compose:1.5.1") + implementation("androidx.compose.material:material:1.2.1") + implementation("androidx.compose.animation:animation:1.2.1") + implementation("androidx.compose.ui:ui-tooling:1.2.1") +} + +/** + * For some reason, the android compose feature (enabled by buildFeatures.compose = true in the demo plugin) + * does not work with the KMP plugin, only with kotlin-android. There are a few tickets that were closed, + * like this for instance: https://issuetracker.google.com/issues/155536223 + * Workaround is to add the compose compiler plugin to the plugin classpath (could be done with freeCompilerArgs). + */ +/* dependencies { + val composeCompilerDependency = "androidx.compose.compiler:compiler:${android.composeOptions.kotlinCompilerExtensionVersion!!}" + configurations.configureEach { + if (name == "kotlinCompilerPluginClasspathAndroidDebug") add(name, composeCompilerDependency) + if (name == "kotlinCompilerPluginClasspathAndroidRelease") add(name, composeCompilerDependency) + } +} */ + +val c by tasks.registering { + dependsOn("compileKotlinAndroidNativeArm32") +} + +val l by tasks.registering { + dependsOn("linkDebugSharedAndroidNativeArm32") +} + +/** + * This is to make included parent build work. Kotlin Compiler Plugins have a configuration + * issue: https://youtrack.jetbrains.com/issue/KT-53477/ . We workaround this in kotlin-compiler-plugin + * by using a fat JAR, but this fat JAR is exported from the "shadow" configuration, while included builds + * read from the "default" configuration and there doesn't seem to be a clean way to solve this. + */ +configurations.matching { + it.name.startsWith("kotlin") && it.name.contains("CompilerPluginClasspath") +}.all { + isTransitive = true +} \ No newline at end of file diff --git a/experiments/expect-actual/src/androidMain/AndroidManifest.xml b/experiments/expect-actual/src/androidMain/AndroidManifest.xml new file mode 100644 index 0000000..36df51f --- /dev/null +++ b/experiments/expect-actual/src/androidMain/AndroidManifest.xml @@ -0,0 +1,16 @@ + + + + + + + + + + \ No newline at end of file diff --git a/experiments/expect-actual/src/androidMain/kotlin/io/deepmedia/tools/knee/sample/RootScreen.kt b/experiments/expect-actual/src/androidMain/kotlin/io/deepmedia/tools/knee/sample/RootScreen.kt new file mode 100644 index 0000000..d7e79df --- /dev/null +++ b/experiments/expect-actual/src/androidMain/kotlin/io/deepmedia/tools/knee/sample/RootScreen.kt @@ -0,0 +1,44 @@ +package io.deepmedia.tools.knee.sample + +import androidx.activity.compose.setContent +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import kotlinx.coroutines.flow.flowOf + + +class ExpectActualActivity : androidx.activity.ComponentActivity() { + companion object { + init { + System.loadLibrary("expect_actual") + } + } + + override fun onCreate(savedInstanceState: android.os.Bundle?) { + super.onCreate(savedInstanceState) + setContent { + MaterialTheme(colors = lightColors()) { + Surface { + RootScreen(Modifier.fillMaxSize()) + } + } + } + } +} + + +@Composable +fun RootScreen(modifier: Modifier = Modifier) { + Column(modifier) { + Text(jvmToString()) + Text(targetName()) + Text(PlatformInfoA().targetName) + Text(PlatformInfoB().targetName) + } +} \ No newline at end of file diff --git a/experiments/expect-actual/src/androidNativeArm32Main/kotlin/Actual.kt b/experiments/expect-actual/src/androidNativeArm32Main/kotlin/Actual.kt new file mode 100644 index 0000000..dd0b9da --- /dev/null +++ b/experiments/expect-actual/src/androidNativeArm32Main/kotlin/Actual.kt @@ -0,0 +1,22 @@ +package io.deepmedia.tools.knee.sample + +import io.deepmedia.tools.knee.annotations.* + +@Knee +actual fun targetName() = "androidNativeArm32" + +actual typealias NativePlatformInfoA = Arm32PlatformInfo + +class Arm32PlatformInfo @Knee constructor() : PlatformInfo() { + override val targetName: String = "androidNativeArm32" +} + +actual class NativePlatformInfoB @Knee constructor() : PlatformInfo() { + override val targetName: String = "androidNativeArm32" +} + +@KneeClass +actual class FunctionWithDefaultParameter { + @Knee + actual fun doSomethingWithDefaultParameter(parameter: Int) { } +} \ No newline at end of file diff --git a/experiments/expect-actual/src/androidNativeArm64Main/kotlin/Actual.kt b/experiments/expect-actual/src/androidNativeArm64Main/kotlin/Actual.kt new file mode 100644 index 0000000..3ccbe5d --- /dev/null +++ b/experiments/expect-actual/src/androidNativeArm64Main/kotlin/Actual.kt @@ -0,0 +1,16 @@ +package io.deepmedia.tools.knee.sample + +import io.deepmedia.tools.knee.annotations.* + +@Knee +actual fun targetName() = "androidNativeArm64" + +actual typealias NativePlatformInfoA = Arm64PlatformInfo + +class Arm64PlatformInfo @Knee constructor() : PlatformInfo() { + override val targetName: String = "androidNativeArm64" +} + +actual class NativePlatformInfoB @Knee constructor() : PlatformInfo() { + override val targetName: String = "androidNativeArm64" +} \ No newline at end of file diff --git a/experiments/expect-actual/src/androidNativeX64Main/kotlin/Actual.kt b/experiments/expect-actual/src/androidNativeX64Main/kotlin/Actual.kt new file mode 100644 index 0000000..ad401ae --- /dev/null +++ b/experiments/expect-actual/src/androidNativeX64Main/kotlin/Actual.kt @@ -0,0 +1,16 @@ +package io.deepmedia.tools.knee.sample + +import io.deepmedia.tools.knee.annotations.* + +@Knee +actual fun targetName() = "androidNativeX64" + +actual typealias NativePlatformInfoA = X64PlatformInfo + +class X64PlatformInfo @Knee constructor() : PlatformInfo() { + override val targetName: String = "androidNativeX64" +} + +actual class NativePlatformInfoB @Knee constructor() : PlatformInfo() { + override val targetName: String = "androidNativeX64" +} \ No newline at end of file diff --git a/experiments/expect-actual/src/androidNativeX86Main/kotlin/Actual.kt b/experiments/expect-actual/src/androidNativeX86Main/kotlin/Actual.kt new file mode 100644 index 0000000..3c44b0a --- /dev/null +++ b/experiments/expect-actual/src/androidNativeX86Main/kotlin/Actual.kt @@ -0,0 +1,16 @@ +package io.deepmedia.tools.knee.sample + +import io.deepmedia.tools.knee.annotations.* + +@Knee +actual fun targetName() = "androidNativeX86" + +actual typealias NativePlatformInfoA = X86PlatformInfo + +class X86PlatformInfo @Knee constructor() : PlatformInfo() { + override val targetName: String = "androidNativeX86" +} + +actual class NativePlatformInfoB @Knee constructor() : PlatformInfo() { + override val targetName: String = "androidNativeX86" +} \ No newline at end of file diff --git a/experiments/expect-actual/src/backendMain/kotlin/Expect.kt b/experiments/expect-actual/src/backendMain/kotlin/Expect.kt new file mode 100644 index 0000000..ce74ac6 --- /dev/null +++ b/experiments/expect-actual/src/backendMain/kotlin/Expect.kt @@ -0,0 +1,25 @@ +package io.deepmedia.tools.knee.sample + +import io.deepmedia.tools.knee.annotations.* +import io.deepmedia.tools.knee.runtime.* +import kotlinx.cinterop.ExperimentalForeignApi + +expect fun targetName(): String + +@OptIn(ExperimentalForeignApi::class) +@Knee +fun jvmToString(): String = currentJavaVirtualMachine.toString() + +@KneeClass(name = "PlatformInfoA") +expect class NativePlatformInfoA : PlatformInfo + +@KneeClass(name = "PlatformInfoB") +expect class NativePlatformInfoB : PlatformInfo + +abstract class PlatformInfo { + @Knee abstract val targetName: String +} + +expect class FunctionWithDefaultParameter { + fun doSomethingWithDefaultParameter(parameter: Int = 0) +} \ No newline at end of file diff --git a/experiments/expect-actual/src/backendMain/kotlin/Init.kt b/experiments/expect-actual/src/backendMain/kotlin/Init.kt new file mode 100644 index 0000000..6b27eb7 --- /dev/null +++ b/experiments/expect-actual/src/backendMain/kotlin/Init.kt @@ -0,0 +1,17 @@ +@file:OptIn(ExperimentalForeignApi::class) + +package io.deepmedia.tools.knee.sample + +import io.deepmedia.tools.knee.annotations.* +import io.deepmedia.tools.knee.runtime.currentJavaVirtualMachine +import kotlinx.cinterop.ExperimentalForeignApi +import platform.android.ANDROID_LOG_WARN +import platform.android.__android_log_print + +@OptIn(ExperimentalStdlibApi::class) +@CName(externName = "JNI_OnLoad") +@KneeInit +fun initKnee() { + __android_log_print(ANDROID_LOG_WARN.toInt(), "Sample", "Hello") + __android_log_print(ANDROID_LOG_WARN.toInt(), "Sample", "Hello $currentJavaVirtualMachine") +} diff --git a/experiments/gradle.properties b/experiments/gradle.properties new file mode 100644 index 0000000..ed29054 --- /dev/null +++ b/experiments/gradle.properties @@ -0,0 +1,6 @@ +kotlin.mpp.stability.nowarn=true +android.useAndroidX=true +org.gradle.caching=true +kotlin.incremental.useClasspathSnapshot=true +kotlin.mpp.import.enableKgpDependencyResolution=true +org.gradle.jvmargs=-Xmx1024m -XX:MaxPermSize=512m -XX:MaxMetaspaceSize=512m \ No newline at end of file diff --git a/experiments/gradle/wrapper/gradle-wrapper.jar b/experiments/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..7454180 Binary files /dev/null and b/experiments/gradle/wrapper/gradle-wrapper.jar differ diff --git a/experiments/gradle/wrapper/gradle-wrapper.properties b/experiments/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..27313fb --- /dev/null +++ b/experiments/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/experiments/gradlew b/experiments/gradlew new file mode 100755 index 0000000..744e882 --- /dev/null +++ b/experiments/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or 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 +# +# https://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. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MSYS* | MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/experiments/gradlew.bat b/experiments/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/experiments/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/experiments/multimodule-consumer/.gitignore b/experiments/multimodule-consumer/.gitignore new file mode 100644 index 0000000..f3d6549 --- /dev/null +++ b/experiments/multimodule-consumer/.gitignore @@ -0,0 +1 @@ +/build/ \ No newline at end of file diff --git a/experiments/multimodule-consumer/build.gradle.kts b/experiments/multimodule-consumer/build.gradle.kts new file mode 100644 index 0000000..de2bc96 --- /dev/null +++ b/experiments/multimodule-consumer/build.gradle.kts @@ -0,0 +1,101 @@ +import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget + +plugins { + kotlin("multiplatform") + id("com.android.application") + id("io.deepmedia.tools.knee") version "0.2.0-SNAPSHOT" +} + +configurations.configureEach { + resolutionStrategy.cacheChangingModulesFor(0, "seconds") +} + +android { + namespace = "io.deepmedia.tools.knee.sample.mm.consumer" + compileSdk = 33 + defaultConfig { + minSdk = 26 + targetSdk = 33 + } + sourceSets { + configureEach { + kotlin.srcDir("src/android${name.capitalize()}/kotlin") + manifest.srcFile("src/android${name.capitalize()}/AndroidManifest.xml") + } + } + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = "1.4.7" + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} + +knee { + enabled.set(true) + verbose.set(true) + autoBind.set(true) +} + +kotlin { + jvmToolchain(11) + + // frontend + androidTarget() + + sourceSets.commonMain.configure { + + dependencies { + api(project(":multimodule-producer")) + } + } + + // backend + val configureBackendTarget: KotlinNativeTarget.() -> Unit = { + fun KotlinCompilation<*>.configureBackendSourceSet() { + val sets = kotlin.sourceSets + val parent = sets.maybeCreate("backend${name.capitalize()}") + parent.dependsOn(sets["common${name.capitalize()}"]) + defaultSourceSet.dependsOn(parent) + } + compilations[KotlinCompilation.MAIN_COMPILATION_NAME].configureBackendSourceSet() + compilations[KotlinCompilation.TEST_COMPILATION_NAME].configureBackendSourceSet() + } + androidNativeArm32(configure = configureBackendTarget) + androidNativeArm64(configure = configureBackendTarget) + androidNativeX64(configure = configureBackendTarget) + androidNativeX86(configure = configureBackendTarget) +} + +dependencies { + implementation("androidx.core:core-ktx:1.9.0") + implementation("androidx.activity:activity-compose:1.5.1") + implementation("androidx.compose.material:material:1.2.1") + implementation("androidx.compose.animation:animation:1.2.1") + implementation("androidx.compose.ui:ui-tooling:1.2.1") +} + +val c by tasks.registering { + dependsOn("compileKotlinAndroidNativeArm32") +} + +val l by tasks.registering { + dependsOn("linkDebugSharedAndroidNativeArm32") +} + +/** + * This is to make included parent build work. Kotlin Compiler Plugins have a configuration + * issue: https://youtrack.jetbrains.com/issue/KT-53477/ . We workaround this in kotlin-compiler-plugin + * by using a fat JAR, but this fat JAR is exported from the "shadow" configuration, while included builds + * read from the "default" configuration and there doesn't seem to be a clean way to solve this. + */ +configurations.matching { + it.name.startsWith("kotlin") && it.name.contains("CompilerPluginClasspath") +}.all { + isTransitive = true +} \ No newline at end of file diff --git a/experiments/multimodule-consumer/src/androidMain/AndroidManifest.xml b/experiments/multimodule-consumer/src/androidMain/AndroidManifest.xml new file mode 100644 index 0000000..dfabb4b --- /dev/null +++ b/experiments/multimodule-consumer/src/androidMain/AndroidManifest.xml @@ -0,0 +1,16 @@ + + + + + + + + + + \ No newline at end of file diff --git a/experiments/multimodule-consumer/src/androidMain/kotlin/RootScreen.kt b/experiments/multimodule-consumer/src/androidMain/kotlin/RootScreen.kt new file mode 100644 index 0000000..b2fe36d --- /dev/null +++ b/experiments/multimodule-consumer/src/androidMain/kotlin/RootScreen.kt @@ -0,0 +1,66 @@ +package io.deepmedia.tools.knee.sample.mm.consumer + +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.lightColors +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import io.deepmedia.tools.knee.mm.consumer.ConsumerEnum +import io.deepmedia.tools.knee.mm.consumer.getConsumerEnum +import io.deepmedia.tools.knee.mm.consumer.getProducerClassExportedByConsumer +import io.deepmedia.tools.knee.mm.consumer.getProducerEnumExportedByConsumer +import io.deepmedia.tools.knee.mm.consumer.getProducerInterfaceExportedByConsumer +import io.deepmedia.tools.knee.sample.mm.producer.ProducerFrontendEnum +import io.deepmedia.tools.knee.sample.mm.producer.getProducerEnum + + +class ConsumerActivity : androidx.activity.ComponentActivity() { + companion object { + init { + System.loadLibrary("multimodule_consumer") + } + } + + override fun onCreate(savedInstanceState: android.os.Bundle?) { + super.onCreate(savedInstanceState) + setContent { + MaterialTheme(colors = lightColors()) { + Surface { + RootScreen(Modifier.fillMaxSize()) + } + } + } + } +} + +@Composable +fun RootScreen(modifier: Modifier = Modifier) { + Column(modifier) { + Text("Consumer enum:") + Text(getConsumerEnum().toString()) + Spacer(Modifier.height(16.dp)) + + Text("Producer enum (provided by its own module):") + Text(getProducerEnum().toString()) + Spacer(Modifier.height(16.dp)) + + Text("Producer enum (provided by consumer module):") + Text(getProducerEnumExportedByConsumer().toString()) + Spacer(Modifier.height(16.dp)) + + Text("Producer interface (provided by consumer module):") + Text(getProducerInterfaceExportedByConsumer().toString()) + Spacer(Modifier.height(16.dp)) + + Text("Producer class (provided by consumer module):") + Text(getProducerClassExportedByConsumer().toString()) + Spacer(Modifier.height(16.dp)) + } +} \ No newline at end of file diff --git a/experiments/multimodule-consumer/src/backendMain/kotlin/Init.kt b/experiments/multimodule-consumer/src/backendMain/kotlin/Init.kt new file mode 100644 index 0000000..4dbfafd --- /dev/null +++ b/experiments/multimodule-consumer/src/backendMain/kotlin/Init.kt @@ -0,0 +1,45 @@ +package io.deepmedia.tools.knee.mm.consumer + +import io.deepmedia.tools.knee.annotations.* +import io.deepmedia.tools.knee.runtime.* +import io.deepmedia.tools.knee.sample.mm.producer.ProducerClass +import io.deepmedia.tools.knee.sample.mm.producer.ProducerEnum +import io.deepmedia.tools.knee.sample.mm.producer.ProducerInterface +import io.deepmedia.tools.knee.sample.mm.producer.initProducerKnee +import kotlin.random.Random + +@CName(externName = "JNI_OnLoad") +@KneeInit +fun initKnee(jvm: JavaVirtualMachine) { + jvm.useEnv { env -> + initProducerKnee(env) + } +} + +@KneeEnum +enum class ConsumerEnum { + Foo, Bar +} + +@Knee +fun getConsumerEnum(): ConsumerEnum { + return ConsumerEnum.Foo +} + +@Knee +fun getProducerEnumExportedByConsumer(): ProducerEnum { + return ProducerEnum.Bar +} + +@Knee +fun getProducerClassExportedByConsumer(): ProducerClass { + return ProducerClass() +} + +@Knee +fun getProducerInterfaceExportedByConsumer(): ProducerInterface { + return object : ProducerInterface { + override val foo: Int + get() = 20 + } +} \ No newline at end of file diff --git a/experiments/multimodule-producer/.gitignore b/experiments/multimodule-producer/.gitignore new file mode 100644 index 0000000..f3d6549 --- /dev/null +++ b/experiments/multimodule-producer/.gitignore @@ -0,0 +1 @@ +/build/ \ No newline at end of file diff --git a/experiments/multimodule-producer/build.gradle.kts b/experiments/multimodule-producer/build.gradle.kts new file mode 100644 index 0000000..e34cdf6 --- /dev/null +++ b/experiments/multimodule-producer/build.gradle.kts @@ -0,0 +1,79 @@ +import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget + +plugins { + kotlin("multiplatform") + id("com.android.library") + id("io.deepmedia.tools.knee") version "0.2.0-SNAPSHOT" +} + +configurations.configureEach { + resolutionStrategy.cacheChangingModulesFor(0, "seconds") +} + +android { + namespace = "io.deepmedia.tools.knee.sample.mm.producer" + compileSdk = 33 + defaultConfig { + minSdk = 26 + targetSdk = 33 + } + sourceSets { + configureEach { + kotlin.srcDir("src/android${name.capitalize()}/kotlin") + manifest.srcFile("src/android${name.capitalize()}/AndroidManifest.xml") + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} + +knee { + enabled.set(true) + verbose.set(true) + autoBind.set(true) +} + +kotlin { + jvmToolchain(11) + // frontend + androidTarget() + + // backend + val configureBackendTarget: KotlinNativeTarget.() -> Unit = { + fun KotlinCompilation<*>.configureBackendSourceSet() { + val sets = kotlin.sourceSets + val parent = sets.maybeCreate("backend${name.capitalize()}") + parent.dependsOn(sets["common${name.capitalize()}"]) + defaultSourceSet.dependsOn(parent) + } + compilations[KotlinCompilation.MAIN_COMPILATION_NAME].configureBackendSourceSet() + compilations[KotlinCompilation.TEST_COMPILATION_NAME].configureBackendSourceSet() + } + androidNativeArm32(configure = configureBackendTarget) + androidNativeArm64(configure = configureBackendTarget) + androidNativeX64(configure = configureBackendTarget) + androidNativeX86(configure = configureBackendTarget) +} + +val c by tasks.registering { + dependsOn("compileKotlinAndroidNativeArm32") +} + +val l by tasks.registering { + dependsOn("linkDebugSharedAndroidNativeArm32") +} + +/** + * This is to make included parent build work. Kotlin Compiler Plugins have a configuration + * issue: https://youtrack.jetbrains.com/issue/KT-53477/ . We workaround this in kotlin-compiler-plugin + * by using a fat JAR, but this fat JAR is exported from the "shadow" configuration, while included builds + * read from the "default" configuration and there doesn't seem to be a clean way to solve this. + */ +configurations.matching { + it.name.startsWith("kotlin") && it.name.contains("CompilerPluginClasspath") +}.all { + isTransitive = true +} \ No newline at end of file diff --git a/experiments/multimodule-producer/src/androidMain/AndroidManifest.xml b/experiments/multimodule-producer/src/androidMain/AndroidManifest.xml new file mode 100644 index 0000000..972c3a8 --- /dev/null +++ b/experiments/multimodule-producer/src/androidMain/AndroidManifest.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/experiments/multimodule-producer/src/androidMain/kotlin/ProducerFrontend.kt b/experiments/multimodule-producer/src/androidMain/kotlin/ProducerFrontend.kt new file mode 100644 index 0000000..dad73dc --- /dev/null +++ b/experiments/multimodule-producer/src/androidMain/kotlin/ProducerFrontend.kt @@ -0,0 +1,5 @@ +package io.deepmedia.tools.knee.sample.mm.producer + +enum class ProducerFrontendEnum { + FrontendFoo, FrontendBar +} \ No newline at end of file diff --git a/experiments/multimodule-producer/src/backendMain/kotlin/Init.kt b/experiments/multimodule-producer/src/backendMain/kotlin/Init.kt new file mode 100644 index 0000000..7cf19d3 --- /dev/null +++ b/experiments/multimodule-producer/src/backendMain/kotlin/Init.kt @@ -0,0 +1,29 @@ +package io.deepmedia.tools.knee.sample.mm.producer + +import io.deepmedia.tools.knee.annotations.* +import io.deepmedia.tools.knee.runtime.* + +@KneeInit +fun initProducerKnee(env: JniEnvironment) { + +} + +@KneeEnum(exported = true) +enum class ProducerEnum { + Foo, Bar +} + +@KneeClass(exported = true) +class ProducerClass { + fun asd() = 2 +} + +@KneeInterface(exported = true) +interface ProducerInterface { + val foo: Int +} + +@Knee +fun getProducerEnum(): ProducerEnum { + return ProducerEnum.Foo +} \ No newline at end of file diff --git a/experiments/settings.gradle.kts b/experiments/settings.gradle.kts new file mode 100644 index 0000000..271afef --- /dev/null +++ b/experiments/settings.gradle.kts @@ -0,0 +1,32 @@ +pluginManagement { + repositories { + mavenCentral() + gradlePluginPortal() + google() + mavenLocal() + } + plugins { + kotlin("multiplatform") version "1.9.23" apply false + kotlin("jvm") version "1.9.23" apply false + id("com.android.application") version "8.1.1" apply false + } +} + +enableFeaturePreview("STABLE_CONFIGURATION_CACHE") + +dependencyResolutionManagement { + repositories { + mavenCentral() + mavenLocal() + google() + } +} + +includeBuild("..") + +include("compose-notes") +include("expect-actual") +include("multimodule-producer") +include("multimodule-consumer") + +rootProject.name = "KneeSamples" \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..8fb2926 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,12 @@ +kotlin.mpp.stability.nowarn=true +# kotlin.native.useEmbeddableCompilerJar=true +# ^ defaults to true in 1.7.0 +org.gradle.caching=true +org.gradle.caching.debug=false +org.gradle.parallel=true +kotlin.incremental.useClasspathSnapshot=true +kotlin.mpp.enableCInteropCommonization=true +org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m + +knee.version=1.0.0-rc1 +knee.group=io.deepmedia.tools.knee diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..7454180 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..27313fb --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..744e882 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or 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 +# +# https://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. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MSYS* | MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/knee-annotations/.gitignore b/knee-annotations/.gitignore new file mode 100644 index 0000000..f3d6549 --- /dev/null +++ b/knee-annotations/.gitignore @@ -0,0 +1 @@ +/build/ \ No newline at end of file diff --git a/knee-annotations/build.gradle.kts b/knee-annotations/build.gradle.kts new file mode 100644 index 0000000..0ae848e --- /dev/null +++ b/knee-annotations/build.gradle.kts @@ -0,0 +1,39 @@ +import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget + +plugins { + kotlin("multiplatform") + `maven-publish` + id("io.deepmedia.tools.deployer") +} + +kotlin { + applyDefaultHierarchyTemplate { + common { + group("backend") { + withNative() + } + } + } + + // native targets + androidNativeArm32() + androidNativeArm64() + androidNativeX64() + androidNativeX86() + // linuxX64() + // mingwX64() + // macosArm64() + // macosX64() + + // for other consumers + jvmToolchain(11) + jvm(name = "frontend") +} + +deployer { + content.kotlinComponents { + emptyDocs() + } +} + diff --git a/knee-annotations/src/backendMain/kotlin/Knee.kt b/knee-annotations/src/backendMain/kotlin/Knee.kt new file mode 100644 index 0000000..d195d34 --- /dev/null +++ b/knee-annotations/src/backendMain/kotlin/Knee.kt @@ -0,0 +1,59 @@ +@file:Suppress("unused") + +package io.deepmedia.tools.knee.annotations + + +@Target( + AnnotationTarget.FUNCTION, + AnnotationTarget.CONSTRUCTOR, + /** + * Allows annotating a property with: + * @property:Knee + * val prop: Int = 42 + */ + AnnotationTarget.PROPERTY, + /** + * Allows annotating a property with: + * @Knee + * val prop: Int = 42 + * Like [AnnotationTarget.PROPERTY], the declaration can be found during visitIrProperty + * so apparently we don't need special logic for this case. + */ + AnnotationTarget.FIELD) +@Retention(AnnotationRetention.BINARY) +annotation class Knee + +/** + * This annotation is used internally only. + */ +@Retention(AnnotationRetention.BINARY) +annotation class KneeMetadata(val metadata: String) + +@Target( + AnnotationTarget.CLASS, + AnnotationTarget.TYPEALIAS +) +@Retention(AnnotationRetention.BINARY) +annotation class KneeEnum(val name: String = "") + +@Target( + AnnotationTarget.CLASS, + AnnotationTarget.TYPEALIAS +) +@Retention(AnnotationRetention.BINARY) +annotation class KneeClass(val name: String = "") + +@Target( + AnnotationTarget.CLASS, + AnnotationTarget.TYPEALIAS +) +@Retention(AnnotationRetention.BINARY) +annotation class KneeInterface(val name: String = "") + +@Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.BINARY) +annotation class KneeRaw(val name: String) + + + + diff --git a/knee-annotations/src/commonMain/kotlin/Knee.common.kt b/knee-annotations/src/commonMain/kotlin/Knee.common.kt new file mode 100644 index 0000000..fb6e1b0 --- /dev/null +++ b/knee-annotations/src/commonMain/kotlin/Knee.common.kt @@ -0,0 +1,2 @@ +package io.deepmedia.tools.knee.annotations + diff --git a/knee-compiler-plugin/.gitignore b/knee-compiler-plugin/.gitignore new file mode 100644 index 0000000..f3d6549 --- /dev/null +++ b/knee-compiler-plugin/.gitignore @@ -0,0 +1 @@ +/build/ \ No newline at end of file diff --git a/knee-compiler-plugin/build.gradle.kts b/knee-compiler-plugin/build.gradle.kts new file mode 100644 index 0000000..4b5ea7f --- /dev/null +++ b/knee-compiler-plugin/build.gradle.kts @@ -0,0 +1,40 @@ +plugins { + kotlin("jvm") + kotlin("plugin.serialization") + id("io.deepmedia.tools.deployer") + id("com.github.johnrengelman.shadow") version "8.1.1" +} + +dependencies { + compileOnly("org.jetbrains.kotlin:kotlin-compiler-embeddable") + implementation("com.squareup:kotlinpoet:1.12.0") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") +} + +// Annoying configuration needed because of https://youtrack.jetbrains.com/issue/KT-53477/ +// Compiler plugins can't have dependency in Native, unless we use a fat jar. +tasks.shadowJar.configure { + // Remove the -all suffix, otherwise the plugin jar is not picked up + // (very important. it won't throw an error either, just won't apply) + archiveClassifier.set("") + // But also change the destination directory (normally: build/libs), otherwise our output jar + // will overwrite the `jar` task output (which has no classifier), and when two tasks have the same + // outputs, Gradle can go crazy. Example: + // + // Task ':knee-compiler-plugin:signArtifacts0ForLocalPublication' uses this output of task + // ':knee-compiler-plugin:jar' without declaring an explicit or implicit dependency. + // This can lead to incorrect results being produced, depending on what order the tasks are executed. + destinationDirectory.set(layout.buildDirectory.get().dir("libs").dir("shadow")) +} + +deployer { + content { + component { + fromArtifactSet { + artifact(tasks.shadowJar) + } + kotlinSources() + emptyDocs() + } + } +} diff --git a/knee-compiler-plugin/src/main/kotlin/Classes.kt b/knee-compiler-plugin/src/main/kotlin/Classes.kt new file mode 100644 index 0000000..572b439 --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/Classes.kt @@ -0,0 +1,130 @@ +package io.deepmedia.tools.knee.plugin.compiler + +import com.squareup.kotlinpoet.* +import io.deepmedia.tools.knee.plugin.compiler.codegen.CodegenClass +import io.deepmedia.tools.knee.plugin.compiler.codegen.KneeCodegen +import io.deepmedia.tools.knee.plugin.compiler.context.KneeContext +import io.deepmedia.tools.knee.plugin.compiler.symbols.KneeSymbols +import io.deepmedia.tools.knee.plugin.compiler.features.KneeClass +import io.deepmedia.tools.knee.plugin.compiler.jni.JniType +import io.deepmedia.tools.knee.plugin.compiler.codec.CodegenCodecContext +import io.deepmedia.tools.knee.plugin.compiler.codec.IrCodecContext +import io.deepmedia.tools.knee.plugin.compiler.codec.Codec +import io.deepmedia.tools.knee.plugin.compiler.export.v1.ExportAdapters +import io.deepmedia.tools.knee.plugin.compiler.instances.InstancesCodegen +import io.deepmedia.tools.knee.plugin.compiler.instances.InstancesCodegen.addHandleConstructorAndField +import io.deepmedia.tools.knee.plugin.compiler.instances.InstancesCodegen.addObjectOverrides +import io.deepmedia.tools.knee.plugin.compiler.symbols.RuntimeIds.decodeClass +import io.deepmedia.tools.knee.plugin.compiler.symbols.RuntimeIds.encodeClass +import io.deepmedia.tools.knee.plugin.compiler.utils.asModifier +import io.deepmedia.tools.knee.plugin.compiler.utils.asTypeSpec +import io.deepmedia.tools.knee.plugin.compiler.utils.codegenFqName +import org.jetbrains.kotlin.ir.builders.* +import org.jetbrains.kotlin.ir.declarations.IrClass +import org.jetbrains.kotlin.ir.declarations.IrValueDeclaration +import org.jetbrains.kotlin.ir.expressions.IrExpression +import org.jetbrains.kotlin.ir.symbols.IrFunctionSymbol +import org.jetbrains.kotlin.ir.util.* +import org.jetbrains.kotlin.types.KotlinType + +fun preprocessClass(klass: KneeClass, context: KneeContext) { + context.mapper.register(ClassCodec( + symbols = context.symbols, + irClass = klass.source, + irConstructors = klass.constructors.map { it.source.symbol }, + )) +} + +fun processClass(klass: KneeClass, context: KneeContext, codegen: KneeCodegen, initInfo: InitInfo) { + klass.makeCodegen(codegen) + if (klass.isThrowable && klass.importInfo == null) { + initInfo.serializableException(klass.source) + } + if (!context.useExport2) { + ExportAdapters.exportIfNeeded(klass.source, context, codegen, klass.importInfo) + } +} + +/** + * We must create a copy of the class and care about construction and destruction. + * 1. class primary constructor must be one accepting a long reference. + * This also means that we must disallow constructor(Long) in native code as it would clash. + * Then the long term solution can be to create an inline wrapper for Long in JVM runtime. + * 2. Add one secondary constructor per each native constructor. These constructors + * should call into the native constructors that return a long, and forward that to the primary. + * Support for this is mostly built into KneeFunction. + * 3. Add a dispose() function that calls into the native disposer. Not much to do here + * because we already create a KneeFunction for it. Just make sure it gets the right name. + */ +private fun KneeClass.makeCodegen(codegen: KneeCodegen) { + val container = codegen.prepareContainer(source, importInfo) + codegenClone = container.addChildIfNeeded(CodegenClass(source.asTypeSpec())).apply { + if (codegen.verbose) spec.addKdoc("knee:classes") + spec.addModifiers(source.visibility.asModifier()) + spec.addHandleConstructorAndField(preserveSymbols = isThrowable) // for exception handling + spec.addObjectOverrides(codegen.verbose) + if (isThrowable) { + spec.superclass(THROWABLE) + } + codegenProducts.add(this) + } +} + +class ClassCodec( + symbols: KneeSymbols, + private val irClass: IrClass, + private val irConstructors: List, +) : Codec(irClass.defaultType, JniType.Long(symbols)) { + + companion object { + fun encodedTypeForFir(module: org.jetbrains.kotlin.descriptors.ModuleDescriptor): KotlinType { + return module.builtIns.longType + } + fun encodedTypeForIr(symbols: KneeSymbols): JniType { + return JniType.Long(symbols) + } + } + + private val encode = symbols.functions(encodeClass).single() + private val decode = symbols.functions(decodeClass).single() + + /** + * This class is being returned from some function, which might be a constructor. + * We must create a stable ref for this class so that it can be passed to the frontend. + * In addition, if this class is owned by some other, we must add the stable ref to the owner list. + */ + override fun IrStatementsBuilder<*>.irEncode(irContext: IrCodecContext, local: IrValueDeclaration): IrExpression { + return irCall(encode).apply { + putValueArgument(0, irGet(local)) + } + } + + // NOTE: in theory it is possible here to check whether this is a disposer and if it is, + // release the stable refs here. + override fun IrStatementsBuilder<*>.irDecode(irContext: IrCodecContext, jni: IrValueDeclaration): IrExpression { + return irCall(decode).apply { + putTypeArgument(0, localIrType) + putValueArgument(0, irGet(jni)) + } + } + + /** + * A long reference was returned by native code. Here we must call the constructor of our class + * accepting the reference. If this is a constructor, we should instead call this(knee = $bridge), + * which means returning bridge value with no edits. + */ + override fun CodeBlock.Builder.codegenDecode(codegenContext: CodegenCodecContext, jni: String): String { + val isConstructor = codegenContext.functionSymbol in irConstructors + return when { + isConstructor -> jni + else -> "${irClass.codegenFqName}(`${InstancesCodegen.HandleField}` = $jni)" + } + } + + /** + * A JVM class must reach the native world. This means that we must pass the native reference instead. + */ + override fun CodeBlock.Builder.codegenEncode(codegenContext: CodegenCodecContext, local: String): String { + return "$local.`${InstancesCodegen.HandleField}`" + } +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/DownwardFunctions.kt b/knee-compiler-plugin/src/main/kotlin/DownwardFunctions.kt new file mode 100644 index 0000000..346c070 --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/DownwardFunctions.kt @@ -0,0 +1,304 @@ +package io.deepmedia.tools.knee.plugin.compiler + +import com.squareup.kotlinpoet.* +import io.deepmedia.tools.knee.plugin.compiler.codegen.CodegenFunction +import io.deepmedia.tools.knee.plugin.compiler.codegen.KneeCodegen +import io.deepmedia.tools.knee.plugin.compiler.context.KneeContext +import io.deepmedia.tools.knee.plugin.compiler.features.KneeDownwardFunction +import io.deepmedia.tools.knee.plugin.compiler.features.KneeDownwardFunction.Kind +import io.deepmedia.tools.knee.plugin.compiler.functions.DownwardFunctionSignature +import io.deepmedia.tools.knee.plugin.compiler.functions.DownwardFunctionsCodegen +import io.deepmedia.tools.knee.plugin.compiler.functions.DownwardFunctionsIr +import io.deepmedia.tools.knee.plugin.compiler.codec.CodegenCodecContext +import io.deepmedia.tools.knee.plugin.compiler.codec.IrCodecContext +import io.deepmedia.tools.knee.plugin.compiler.context.KneeLogger +import io.deepmedia.tools.knee.plugin.compiler.context.KneeOrigin +import io.deepmedia.tools.knee.plugin.compiler.jni.JniType +import io.deepmedia.tools.knee.plugin.compiler.utils.* +import io.deepmedia.tools.knee.plugin.compiler.symbols.CInteropIds +import io.deepmedia.tools.knee.plugin.compiler.symbols.RuntimeIds.kneeInvokeJvmSuspend +import io.deepmedia.tools.knee.plugin.compiler.symbols.RuntimeIds.rethrowNativeException +import org.jetbrains.kotlin.backend.common.lower.irCatch +import org.jetbrains.kotlin.ir.builders.* +import org.jetbrains.kotlin.ir.builders.declarations.addValueParameter +import org.jetbrains.kotlin.ir.builders.declarations.buildVariable +import org.jetbrains.kotlin.ir.declarations.IrDeclarationOrigin +import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction +import org.jetbrains.kotlin.ir.expressions.impl.IrConstImpl +import org.jetbrains.kotlin.ir.util.* +import org.jetbrains.kotlin.name.Name + +fun processDownwardFunction(function: KneeDownwardFunction, context: KneeContext, codegen: KneeCodegen, initInfo: InitInfo) { + val signature = DownwardFunctionSignature(function.source, function.kind, context) + function.makeIr(context, signature, initInfo) + function.makeCodegen(codegen, signature, context.log) +} + +private fun KneeDownwardFunction.makeCodegen(codegen: KneeCodegen, signature: DownwardFunctionSignature, logger: KneeLogger) { + // Unlike IR, we have to generate both the bridge function and the local function. + // First we make the local function, whose implementation will call the bridge + val localName = source.name.asString() + val bridgeName = signature.jniInfo.name(includeAncestors = false) + + val bridgeSpec: FunSpec.Builder + val localSpec: FunSpec.Builder = when { + source.isSetter -> FunSpec.setterBuilder() + source.isGetter -> FunSpec.getterBuilder() + kind is Kind.ClassConstructor -> FunSpec + .constructorBuilder() + .addModifiers(source.visibility.asModifier()) + else -> FunSpec + .builder(localName) + .addModifiers(source.visibility.asModifier()) + .apply { + if ((source as? IrSimpleFunction)?.isOperator == true) { + addModifiers(KModifier.OPERATOR) + } + if (source.isSuspend) { + addModifiers(KModifier.SUSPEND) + } + if (kind is Kind.InterfaceMember) { + addModifiers(KModifier.OVERRIDE) + } + } + } + localSpec.apply { + // RETURN TYPE + // Add it unless getter or setter or constructor because KotlinPoet will throw in this case + // E.g. 'IllegalStateException: get() cannot have a return type' + signature.result.let { + if (!source.isGetter && !source.isSetter && kind !is Kind.ClassConstructor) { + returns(it.localCodegenType.name) + } + } + // PARAMETERS + // Exclude prefixes, they only refer to bridge functions + signature.regularParameters.forEach { (param, codec) -> + val name = param.asStringSafeForCodegen(true) + val defaultValue = source.valueParameters.firstOrNull { it.name == param }?.defaultValueForCodegen(expectSources) + // addParameter(name, codec.localCodegenType.name) + addParameter(ParameterSpec.builder(name, codec.localCodegenType.name) + .defaultValue(defaultValue) + .build()) + } + // BODY + with(DownwardFunctionsCodegen) { + val codecContext = CodegenCodecContext(source.symbol, false, logger) + if (signature.isSuspend) { + // public suspend fun kneeInvokeJvmSuspend(block: (KneeSuspendInvoker) -> Long): T + addCode(CodeBlock.builder().apply { + val invoke = MemberName("io.deepmedia.tools.knee.runtime.compiler", "kneeInvokeJvmSuspend") + beginControlFlow("val res = %M { ${DownwardFunctionSignature.Extra.SuspendInvoker} ->", invoke) + bridgeSpec = codegenInvoke(signature, bridgeName, "val token = ", codecContext) + codegenReceive("token", signature, "", codecContext, suspendToken = true) + endControlFlow() + // Map the raw jni result from kneeInvokeJvmSuspend into the local world + codegenReceive("res", signature, "return ", codecContext) + }.build()) + } else if (kind is Kind.ClassConstructor) { + callThisConstructor(CodeBlock.builder().apply { + beginControlFlow("Unit.run") + bridgeSpec = codegenInvoke(signature, bridgeName, "val res = ", codecContext) + bridgeSpec.addAnnotation(ClassName.bestGuess("kotlin.jvm.JvmStatic")) + codegenReceive("res", signature, "", codecContext) + endControlFlow() + }.build()) + } else { + addCode(CodeBlock.builder().apply { + bridgeSpec = codegenInvoke(signature, bridgeName, "val res = ", codecContext) + codegenReceive("res", signature, "return ", codecContext) + }.build()) + } + } + } + + // Save products + if (codegen.verbose) localSpec.addKdoc("knee:functions") + if (codegen.verbose) bridgeSpec.addKdoc("knee:functions:bridge") + val localFun = CodegenFunction(localSpec) + val bridgeFun = CodegenFunction(bridgeSpec) + + // TODO: use FunctionSignature.JniInfo.owner for at least one of this of these containers + val localContainer = kind.property?.codegenImplementation ?: when (kind) { + is Kind.InterfaceMember -> kind.owner.codegenImplementation + else -> codegen.prepareContainer(source, kind.importInfo) + } + val bridgeContainer = when (kind) { // skip properties in this case + is Kind.InterfaceMember -> kind.owner.codegenImplementation + is Kind.ClassConstructor -> codegen.prepareContainer(source, kind.importInfo, createCompanionObject = true) + else -> codegen.prepareContainer(source, kind.importInfo, detectPropertyAccessors = false) + } + + localContainer.addChild(localFun) + bridgeContainer.addChild(bridgeFun) + codegenProducts.add(localFun) + codegenProducts.add(bridgeFun) + + if (kind is Kind.InterfaceMember && kind.property == null) { + // IMPORTANT: use unsubstituted params here! In case of generics, the base interface must have the raw + // parameters with raw unknown types. We expose this info in the signature with the unsubstituted prefix. + // Also use addChildIfNeeded for the same reason we do so in knee:properties:abstract-interface-child + // (User might be importing Flow and Flow, but only one function goes to Flow) + val function = FunSpec.builder(source.name.asString()).apply { + if (codegen.verbose) addKdoc("knee:functions:abstract-interface-child") + addModifiers(KModifier.ABSTRACT) + if (signature.isSuspend) addModifiers(KModifier.SUSPEND) + returns(signature.unsubstitutedReturnTypeForCodegen) + signature.unsubstitutedValueParametersForCodegen.forEach { (name, type) -> + addParameter(name.asString(), type) + } + } + kind.owner.codegenClone?.addChildIfNeeded(CodegenFunction(function)) + } +} + +private fun KneeDownwardFunction.makeIr(context: KneeContext, signature: DownwardFunctionSignature, initInfo: InitInfo) { + val file = kind.importInfo?.file ?: source.file + val property = file.addSimpleProperty( + plugin = context.plugin, + type = context.symbols.typeAliasUnwrapped(CInteropIds.COpaquePointer), + name = signature.jniInfo.name(includeAncestors = true) + ) { + val staticCFunctionCall = irCall( + // staticCFunction(...) + callee = context.symbols.functions(CInteropIds.staticCFunction).single { + it.owner.typeParameters.size == 1 + + signature.knPrefixParameters.size + + signature.extraParameters.size + + signature.regularParameters.size + } + ) + // only argument of staticCFunction is a lambda + staticCFunctionCall.putValueArgument(0, irLambda( + context = context, + parent = parent, + content = { lambda -> + // configure lambda and staticCFunction types + var args = 0 + signature.knPrefixParameters.forEach { (name, type) -> + lambda.addValueParameter(name, type, KneeOrigin.KNEE) + staticCFunctionCall.putTypeArgument(args++, type) + } + signature.extraParameters.forEach { (param, codec) -> + val type = codec.encodedType.knOrNull!! + lambda.addValueParameter(param, type) + staticCFunctionCall.putTypeArgument(args++, type) + } + signature.regularParameters.forEach { (param, codec) -> + val type = codec.encodedType.knOrNull!! + val sourceParam = source.valueParameters.first { it.name == param } + // defaultValue = null is very important here because we are changing the type, potentially + lambda.valueParameters += sourceParam.copyTo(lambda, index = args, type = type, name = param, defaultValue = null) + staticCFunctionCall.putTypeArgument(args++, type) + } + run { + val resultOrSuspendResult = (if (signature.isSuspend) signature.suspendResult else signature.result) + .encodedType.knOrNull ?: context.symbols.builtIns.unitType + lambda.returnType = resultOrSuspendResult + staticCFunctionCall.putTypeArgument(args, resultOrSuspendResult) + } + + + // actual body where we call the user-defined function and do mapping + val environment = + lambda.valueParameters.first { it.name == DownwardFunctionSignature.KnPrefix.JniEnvironment } + val codecContext = IrCodecContext( + functionSymbol = source.symbol, + environment = environment, + reverse = false, + logger = context.log + ) + val logPrefix = "Functions.kt(${source.fqNameWhenAvailable})" + context.log.injectLog(this, "$logPrefix CALLED FROM JVM") + +irReturn(if (!signature.isSuspend) { + with(DownwardFunctionsIr) { + // val raw = irInvoke(lambda.valueParameters, source, signature, codecContext) + // irReceive(raw, signature, codecContext) + val catch = buildVariable(parent, SYNTHETIC_OFFSET, SYNTHETIC_OFFSET, IrDeclarationOrigin.CATCH_PARAMETER, Name.identifier("t"), context.symbols.builtIns.throwableType) + irTry( + type = signature.result.encodedType.knOrNull ?: context.symbols.builtIns.unitType, + tryResult = irComposite { + val raw = irInvoke(lambda.valueParameters, source, signature, codecContext) + +irReceive(raw, signature, codecContext) + }, + catches = listOf(irCatch( + catchParameter = catch, + result = irComposite { + // Forward the error to the JVM and swallow it on the native side. + +irCall(context.symbols.functions(rethrowNativeException).single()).apply { + extensionReceiver = irGet(environment) + putValueArgument(0, irGet(catch)) + } + // Return 'something' here otherwise compilation fails (I think). + // It will never be used anyway because the JVM will throw due to previous command. + +when (val type = signature.result.encodedType) { + is JniType.Void -> irUnit() + is JniType.Object -> irNull() + is JniType.Array -> irNull() + is JniType.Int -> irInt(0) + is JniType.Long -> irLong(0) + is JniType.Float -> IrConstImpl.float(startOffset, endOffset, type.kn, 0F) + is JniType.Double -> IrConstImpl.double(startOffset, endOffset, type.kn, 0.0) + is JniType.Byte -> IrConstImpl.byte(startOffset, endOffset, type.kn, 0) + is JniType.BooleanAsUByte -> IrConstImpl.byte(startOffset, endOffset, type.kn, 0) // hope this works... + } + } + )), + finallyExpression = null + ) + } + } else { + val suspendInvoke = context.symbols.functions(kneeInvokeJvmSuspend).single() + val suspendInvoker = + irGet(lambda.valueParameters.first { it.name == DownwardFunctionSignature.Extra.SuspendInvoker }) + val returnType = signature.result + irCall(suspendInvoke.owner).apply { + putTypeArgument(0, returnType.encodedType.knOrNull ?: context.symbols.builtIns.unitType) // raw return type + putTypeArgument(1, returnType.localIrType) // actual return type + putValueArgument(0, irGet(environment)) + putValueArgument(1, suspendInvoker) + putValueArgument(2, irLambda(context, parent, suspend = true) { + it.returnType = returnType.localIrType + with(DownwardFunctionsIr) { + +irReturn(irInvoke(lambda.valueParameters, source, signature, codecContext)) + } + }) + putValueArgument(3, irLambda(context, parent) { + it.returnType = returnType.encodedType.knOrNull ?: context.symbols.builtIns.unitType + it.addValueParameter("_env", environment.type) + it.addValueParameter("_data", returnType.localIrType) + // Need a new context because the local invocation might have suspended and might have returned + // on another thread with no current environment. This is also why we have two lambdas here, so that the + // new environment is provided by the runtime. + val freshCodecContext = IrCodecContext( + functionSymbol = source.symbol, + environment = it.valueParameters[0], + reverse = false, + logger = context.log + ) + val raw = irGet(it.valueParameters[1]) + with(DownwardFunctionsIr) { + +irReturn(irReceive(raw, signature, freshCodecContext)) + } + }) + }.let { call -> + // Technically this is useless, token is a long and needs to conversion, but leaving it + // for clarity and future-proofness. + with(DownwardFunctionsIr) { + irReceive(call, signature, codecContext, suspendToken = true) + } + } + }) + } + )) + staticCFunctionCall + } + irProducts.add(property) + initInfo.registerNative( + context = context, + container = signature.jniInfo.owner, + pointerProperty = property, + methodName = signature.jniInfo.name(includeAncestors = false).asString(), + methodJniSignature = signature.jniInfo.signature, + ) +} diff --git a/knee-compiler-plugin/src/main/kotlin/DownwardProperties.kt b/knee-compiler-plugin/src/main/kotlin/DownwardProperties.kt new file mode 100644 index 0000000..2c37004 --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/DownwardProperties.kt @@ -0,0 +1,63 @@ +package io.deepmedia.tools.knee.plugin.compiler + +import com.squareup.kotlinpoet.KModifier +import io.deepmedia.tools.knee.plugin.compiler.codegen.CodegenProperty +import io.deepmedia.tools.knee.plugin.compiler.codegen.KneeCodegen +import io.deepmedia.tools.knee.plugin.compiler.context.KneeContext +import io.deepmedia.tools.knee.plugin.compiler.features.KneeDownwardProperty +import io.deepmedia.tools.knee.plugin.compiler.import.concrete +import io.deepmedia.tools.knee.plugin.compiler.symbols.KneeSymbols +import io.deepmedia.tools.knee.plugin.compiler.utils.asModifier +import io.deepmedia.tools.knee.plugin.compiler.utils.asPropertySpec +import org.jetbrains.kotlin.descriptors.Modality +import org.jetbrains.kotlin.ir.types.IrSimpleType + +fun processDownwardProperty(property: KneeDownwardProperty, context: KneeContext, codegen: KneeCodegen) { + property.makeCodegen(codegen, context.symbols) +} + +// Create the codegen property. If we don't do this, this would be done anyway +// by codegen.container() when invoked by the setter/getter function. But let's do +// it so we can add appropriate modifiers. +private fun KneeDownwardProperty.makeCodegen(codegen: KneeCodegen, symbols: KneeSymbols) { + fun makeProperty( + typeMapper: (IrSimpleType) -> IrSimpleType = { it }, + kdocSuffix: String = "", + isOverride: Boolean = false + ) = source.asPropertySpec(typeMapper).run { + if (codegen.verbose) addKdoc("knee:properties${kdocSuffix}") + addModifiers(source.visibility.asModifier()) + if (isOverride) addModifiers(KModifier.OVERRIDE) + if (source.modality == Modality.OPEN) addModifiers(KModifier.OPEN) + CodegenProperty(this).also { + codegenProducts.add(it) + } + } + + // Where should the function implementation go? + when (kind) { + is KneeDownwardProperty.Kind.InterfaceMember -> { + // For the abstract child, use addChildIfNeeded. This is because when user imports + // a local type Flow with more than implementation Flow, Flow, we make the codegen + // as the generic Flow and as such A.property and B.property should not write twice there. + val abstract = makeProperty(kdocSuffix = ":abstract-interface-child") + kind.owner.codegenClone?.addChildIfNeeded(abstract) + + val implementation = makeProperty(typeMapper = { it.concrete(kind.importInfo) }) + implementation.spec.addModifiers(KModifier.OVERRIDE) + kind.owner.codegenImplementation.addChild(implementation) + codegenImplementation = implementation + } + is KneeDownwardProperty.Kind.ClassMember -> { + val isOverride = kind.owner.isOverrideInCodegen(symbols, this) + val property = makeProperty(isOverride = isOverride) + codegen.prepareContainer(source, kind.importInfo).addChild(property) + codegenImplementation = property + } + is KneeDownwardProperty.Kind.TopLevel -> { + val property = makeProperty() + codegen.prepareContainer(source, kind.importInfo).addChild(property) + codegenImplementation = property + } + } +} diff --git a/knee-compiler-plugin/src/main/kotlin/Enums.kt b/knee-compiler-plugin/src/main/kotlin/Enums.kt new file mode 100644 index 0000000..246dcef --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/Enums.kt @@ -0,0 +1,96 @@ +package io.deepmedia.tools.knee.plugin.compiler + +import com.squareup.kotlinpoet.CodeBlock +import com.squareup.kotlinpoet.TypeSpec +import io.deepmedia.tools.knee.plugin.compiler.codec.Codec +import io.deepmedia.tools.knee.plugin.compiler.codec.CodegenCodecContext +import io.deepmedia.tools.knee.plugin.compiler.codec.IrCodecContext +import io.deepmedia.tools.knee.plugin.compiler.codegen.CodegenClass +import io.deepmedia.tools.knee.plugin.compiler.codegen.KneeCodegen +import io.deepmedia.tools.knee.plugin.compiler.context.KneeContext +import io.deepmedia.tools.knee.plugin.compiler.symbols.KneeSymbols +import io.deepmedia.tools.knee.plugin.compiler.export.v1.ExportAdapters +import io.deepmedia.tools.knee.plugin.compiler.features.KneeEnum +import io.deepmedia.tools.knee.plugin.compiler.jni.JniType +import io.deepmedia.tools.knee.plugin.compiler.symbols.RuntimeIds.decodeEnum +import io.deepmedia.tools.knee.plugin.compiler.symbols.RuntimeIds.encodeEnum +import io.deepmedia.tools.knee.plugin.compiler.utils.asModifier +import io.deepmedia.tools.knee.plugin.compiler.utils.isPartOf +import org.jetbrains.kotlin.descriptors.ModuleDescriptor +import org.jetbrains.kotlin.ir.builders.IrStatementsBuilder +import org.jetbrains.kotlin.ir.builders.irCall +import org.jetbrains.kotlin.ir.builders.irGet +import org.jetbrains.kotlin.ir.declarations.IrValueDeclaration +import org.jetbrains.kotlin.ir.expressions.IrExpression +import org.jetbrains.kotlin.ir.types.IrSimpleType +import org.jetbrains.kotlin.ir.util.defaultType +import org.jetbrains.kotlin.types.KotlinType + +fun processEnum(enum: KneeEnum, context: KneeContext, codegen: KneeCodegen) { + if (enum.source.isPartOf(context.module)) { + enum.makeCodegenClone(codegen) + } + + val codec = EnumCodec( + symbols = context.symbols, + irType = enum.source.defaultType, + ) + context.mapper.register(codec) + + if (!context.useExport2) { + ExportAdapters.exportIfNeeded(enum.source, context, codegen, enum.importInfo) + } +} + +fun KneeEnum.makeCodegenClone(codegen: KneeCodegen) { + val clone = TypeSpec.enumBuilder(source.name.asString()).run { + addModifiers(source.visibility.asModifier()) + entries.forEach { + addEnumConstant(it.name.asString()) + } + CodegenClass(this) + } + codegen.prepareContainer(source, importInfo).addChild(clone) + codegenProducts.add(clone) +} + +class EnumCodec( + symbols: KneeSymbols, + irType: IrSimpleType, +) : Codec(irType, JniType.Int(symbols)) { + + companion object { + fun encodedTypeForFir(module: ModuleDescriptor): KotlinType { + return module.builtIns.intType + } + + fun encodedTypeForIr(symbols: KneeSymbols): JniType { + return JniType.Int(symbols) + } + } + + private val encode = symbols.functions(encodeEnum).single() + private val decode = symbols.functions(decodeEnum).single() + + override fun IrStatementsBuilder<*>.irDecode(irContext: IrCodecContext, jni: IrValueDeclaration): IrExpression { + return irCall(decode).apply { + putTypeArgument(0, localIrType) + putValueArgument(0, irGet(jni)) + } + } + + override fun IrStatementsBuilder<*>.irEncode(irContext: IrCodecContext, local: IrValueDeclaration): IrExpression { + return irCall(encode).apply { + putTypeArgument(0, localIrType) + putValueArgument(0, irGet(local)) + } + } + + override fun CodeBlock.Builder.codegenDecode(codegenContext: CodegenCodecContext, jni: String): String { + return "kotlin.enums.enumEntries<${localCodegenType.name}>()[$jni]" + } + + override fun CodeBlock.Builder.codegenEncode(codegenContext: CodegenCodecContext, local: String): String { + return "$local.ordinal" + } +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/Init.kt b/knee-compiler-plugin/src/main/kotlin/Init.kt new file mode 100644 index 0000000..3980b6a --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/Init.kt @@ -0,0 +1,359 @@ +package io.deepmedia.tools.knee.plugin.compiler + +import com.squareup.kotlinpoet.* +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import io.deepmedia.tools.knee.plugin.compiler.codegen.CodegenClass +import io.deepmedia.tools.knee.plugin.compiler.codegen.CodegenType +import io.deepmedia.tools.knee.plugin.compiler.codegen.KneeCodegen +import io.deepmedia.tools.knee.plugin.compiler.context.KneeContext +import io.deepmedia.tools.knee.plugin.compiler.symbols.KneeSymbols +import io.deepmedia.tools.knee.plugin.compiler.export.v2.ExportAdapters2 +import io.deepmedia.tools.knee.plugin.compiler.export.v2.ExportedTypeInfo +import io.deepmedia.tools.knee.plugin.compiler.features.KneeInitializer +import io.deepmedia.tools.knee.plugin.compiler.features.KneeModule +import io.deepmedia.tools.knee.plugin.compiler.metadata.ModuleMetadata +import io.deepmedia.tools.knee.plugin.compiler.symbols.KotlinIds +import io.deepmedia.tools.knee.plugin.compiler.symbols.RuntimeIds +import io.deepmedia.tools.knee.plugin.compiler.utils.* +import io.deepmedia.tools.knee.plugin.compiler.symbols.RuntimeIds.JNINativeMethod +import kotlinx.serialization.json.Json +import org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder +import org.jetbrains.kotlin.ir.IrElement +import org.jetbrains.kotlin.ir.IrStatement +import org.jetbrains.kotlin.ir.builders.* +import org.jetbrains.kotlin.ir.declarations.IrClass +import org.jetbrains.kotlin.ir.declarations.IrConstructor +import org.jetbrains.kotlin.ir.declarations.IrProperty +import org.jetbrains.kotlin.ir.expressions.* +import org.jetbrains.kotlin.ir.types.* +import org.jetbrains.kotlin.ir.util.* +import org.jetbrains.kotlin.ir.visitors.IrElementTransformerVoid +import org.jetbrains.kotlin.ir.visitors.IrElementVisitorVoid +import org.jetbrains.kotlin.ir.visitors.acceptChildrenVoid +import org.jetbrains.kotlin.ir.visitors.transformChildrenVoid + + +private val KneeSymbols.moduleClass get() = klass(RuntimeIds.KneeModule).owner +private val KneeSymbols.modulePublicConstructor get() = moduleClass.constructors.first { !it.isPrimary } + +// private val KneeSymbols.moduleBuilderClass get() = klass(Names.runtimeKneeModuleBuilderClass).owner +private val KneeSymbols.moduleBuilderExportFunction get() = functions(RuntimeIds.KneeModuleBuilder_export).single() +private val KneeSymbols.moduleBuilderExportAdapterFunction get() = functions(RuntimeIds.KneeModuleBuilder_exportAdapter).single() + +fun processInit( + context: KneeContext, + codegen: KneeCodegen, + info: InitInfo, +) { + when (info) { + is InitInfo.Module -> { + // There should be only one module, but if for some reason many were provided, process all of them + // Goal: replace the module public constructor with the private one, passing more data to it + // Note that since this is a module, we may also have to deal with exports (while initKnee() apps can't export) + info.modules.forEach { module -> + // This was used to parse IrClass-es from metadata, which had a vararg as first parameter + /* val dependencyTypes: List = metadataAnnotation.getValueArgument(0).varargElements().map { it.classType } + val dependencyExpressions: List = with(builder) { dependencyTypes.map { irGetObject(it.classOrFail) } } */ + + // A few IR things to do: + // 1. replace super constructor KneeModule(...) with our own constructor (which passes more data, e.g. preloads) + // 2. collect export()-ed types and determine their info + // 3. replace export() calls with exportAdapter(adapter) + // 4. write dependency information in the module metadata (via annotation) + // https://github.com/androidx/androidx/blob/fec3b387ce47bad7682d01042c22d1913268c2bc/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerIntrinsicTransformer.kt#L62 + val exportedTypes = mutableListOf() + val dependencyModules = module.collectDependencies() // do before transforming the super constructor! + module.source.transformChildrenVoid(object : IrElementTransformerVoid() { + + // Grab the superclass constructor call. Will call visitDelegatingConstructorCall + override fun visitConstructor(declaration: IrConstructor): IrStatement { + declaration.body!!.transformChildrenVoid(this) + return super.visitConstructor(declaration) + } + + override fun visitDelegatingConstructorCall(expression: IrDelegatingConstructorCall): IrExpression { + val isPublicConstructor = expression.symbol == context.symbols.modulePublicConstructor.symbol + if (isPublicConstructor) { + val builder = DeclarationIrBuilder(context.plugin, expression.symbol) + return builder.irCreateModule( + isSuperclass = true, + symbols = context.symbols, + initInfo = info, + varargDependencies = expression.getValueArgument(0), + builderBlock = expression.getValueArgument(1) + ) + } + return super.visitDelegatingConstructorCall(expression) + } + + override fun visitCall(expression: IrCall): IrExpression { + if (context.useExport2 && expression.symbol == context.symbols.moduleBuilderExportFunction) { + val exportedType = expression.getTypeArgument(0)!!.simple("export()") + val exportedTypeInfo = ExportedTypeInfo(exportedTypes.size, context.mapper.get(exportedType)) + exportedTypes.add(exportedTypeInfo) + val builder = DeclarationIrBuilder(context.plugin, module.source.symbol) + val replacement = builder.irExportAdapter(context, expression.dispatchReceiver!!, exportedTypeInfo) + //println("ORIGINAL MODULE_BUILDER_EXPORT = ${expression.dumpKotlinLike()}") + //println("REPLACEMENT MODULE_BUILDER_EXPORT = ${replacement.dumpKotlinLike()}") + return replacement + } + return super.visitCall(expression) + } + }) + + // Write useful information into the module metadata annotation + val metadata = ModuleMetadata(exportedTypes, dependencyModules) + metadata.write(module.source, context) + + // A few JVM things to do: + // 1. Create codegen module extending KneeModule + // 2. Create codegen adapters and pass them to the module + codegen.makeCodegenModule(module, context, exportedTypes) + } + } + is InitInfo.Initializer -> { + // It's possible to have multiple initKnee() call, for example in a if-else branch. + // We don't care, process all of them and inject a synthetic module + info.initializers.forEach { initializer -> + // Goal: replace initKnee(ENV, dep1, dep2, dep3, ...) with initKnee(ENV, SyntheticModule(dep1, dep2, dep3)) + // TODO: it is wrong to pass the expression symbol, it represents the initKnee() function in runtime module + val builder = DeclarationIrBuilder(context.plugin, initializer.expression.symbol) + val dependencies = initializer.expression.getValueArgument(1) + initializer.expression.putValueArgument(1, builder.irVararg( + elementType = context.symbols.moduleClass.defaultType, + values = listOf(builder.irCreateModule( + isSuperclass = false, + symbols = context.symbols, + initInfo = info, + varargDependencies = dependencies, + builderBlock = null + )) + )) + } + } + } +} + + +sealed class InitInfo { + class Module(val modules: List) : InitInfo() + class Initializer(val initializers: List) : InitInfo() + + fun dependencies(json: Json) = when (this) { + is Module -> modules.flatMap { it.collectDependencies() } + is Initializer -> initializers.flatMap { it.collectDependencies() } + }.associateWith { ModuleMetadata.read(it, json) } + + val preloads = mutableSetOf() + fun preload(types: Collection) { + preloads.addAll(types) + } + + val serializableExceptions = mutableSetOf() + fun serializableException(klass: IrClass) { + serializableExceptions.add(klass) // can't be exported + } + + val registerNativesEntries = mutableListOf() + fun registerNative( + context: KneeContext, + container: CodegenType, + pointerProperty: IrProperty, + methodName: String, + methodJniSignature: String + ) { + context.log.logMessage("registerNative: adding $methodName ($methodJniSignature) in ${container.jvmClassName}") + registerNativesEntries.add(RegisterNativesEntry(container, pointerProperty, methodName, methodJniSignature)) + } + + data class RegisterNativesEntry( + /** + * The class containing this JVM method. It's the parent class + * or the synthetic Kt in case of top level functions. + */ + val container: CodegenType, + /** + * A property returning a static c pointer to the native function. + */ + val pointerProperty: IrProperty, + + val methodName: String, + val methodJniSignature: String, + ) +} + +/** + * Returns either a delegating constructor call or a pure constructor call, + * depending on the [isSuperclass] flag. + */ +private fun DeclarationIrBuilder.irCreateModule( + isSuperclass: Boolean, + symbols: KneeSymbols, + initInfo: InitInfo, + varargDependencies: IrExpression?, + builderBlock: IrExpression? +): IrExpression { + val constructor = symbols.moduleClass.constructors.first { it.isPrimary } + val constructorCall = when { + isSuperclass -> irDelegatingConstructorCall(constructor) + else -> irCallConstructor(constructor.symbol, emptyList()) + } + constructorCall.apply { + // val registerNativeContainers: List + // val registerNativeMethods: List> + val groups = initInfo.registerNativesEntries.groupBy { it.container }.entries.map { it } + putValueArgument(0, irRegisterNativesContainers(symbols, groups.map { it.key })) + putValueArgument(1, irRegisterNativesMethods(symbols, groups.map { it.value })) + + // val preloadFqns: List + putValueArgument(2, irPreloadFqns(symbols, initInfo.preloads)) + + // val exceptions: List + putValueArgument(3, irSerializableExceptions(symbols, initInfo.serializableExceptions)) + + // val dependencies: List + // val block: (KneeModuleBuilder.() -> Unit)? + val dependencies = varargDependencies.varargElements() + putValueArgument(4, irListOf(symbols, symbols.moduleClass.defaultType, dependencies)) + putValueArgument(5, builderBlock ?: irNull()) + + // val dependencies: Array? + // val block: (KneeModuleBuilder.() -> Unit)? + // Note that being a vararg, the expression can actually be null + // putValueArgument(3, dependencies ?: irNull()) + // putValueArgument(4, builderBlock ?: irNull()) + } + return constructorCall +} + +private fun DeclarationIrBuilder.irListOf(symbols: KneeSymbols, type: IrType, contents: List): IrExpression { + val listOf = symbols.functions(KotlinIds.listOf).single { it.owner.valueParameters.singleOrNull()?.isVararg == true } + return irCall(listOf).apply { + putTypeArgument(0, type) + putValueArgument(0, irVararg(type, contents)) + } +} + +private fun DeclarationIrBuilder.irPreloadFqns(symbols: KneeSymbols, preloads: Set): IrExpression { + return irListOf(symbols, symbols.builtIns.stringType, preloads.map { + irString(CodegenType.from(it).jvmClassName) + }) +} + +private fun DeclarationIrBuilder.irSerializableExceptions(symbols: KneeSymbols, classes: Set): IrExpression { + val type = symbols.klass(RuntimeIds.SerializableException) + return irListOf(symbols, type.defaultType, classes.map { + irCallConstructor(type.constructors.single(), emptyList()).apply { + putValueArgument(0, irString(it.classIdOrFail.asFqNameString())) // nativeFqn: String + putValueArgument(1, irString(CodegenType.from(it.defaultType).jvmClassName)) // jvmFqn: String + } + }) +} + +private fun DeclarationIrBuilder.irRegisterNativesContainers(symbols: KneeSymbols, containers: List): IrExpression { + return irListOf(symbols, symbols.builtIns.stringType, containers.map { irString(it.jvmClassName) }) +} + +private fun DeclarationIrBuilder.irRegisterNativesMethods(symbols: KneeSymbols, entriesLists: List>): IrExpression { + val methodClass = symbols.klass(JNINativeMethod) + val methodConstructor = methodClass.constructors.single() + val listOfMethods = symbols.builtIns.listClass.typeWith(methodClass.defaultType) + return irListOf(symbols, + type = symbols.builtIns.listClass.typeWith(listOfMethods), + contents = entriesLists.map { entries -> + irListOf(symbols, + type = listOfMethods, + contents = entries.map { entry -> + irCallConstructor(methodConstructor, emptyList()).apply { + putValueArgument(0, irString(entry.methodName)) + putValueArgument(1, irString(entry.methodJniSignature)) + putValueArgument(2, irCall(entry.pointerProperty.getter!!)) + } + } + ) + } + ) +} + +private fun DeclarationIrBuilder.irExportAdapter( + context: KneeContext, + moduleBuilderInstance: IrExpression, + exportedType: ExportedTypeInfo +): IrExpression { + val function = context.symbols.moduleBuilderExportAdapterFunction + return irCall(function).apply { + dispatchReceiver = moduleBuilderInstance + putTypeArgument(0, exportedType.encodedType.kn) + putTypeArgument(1, exportedType.localIrType) + // dispatchReceiver = irGet(function.owner.parentAsClass.thisReceiver!!) + putValueArgument(0, irInt(exportedType.id)) + putValueArgument(1, with(ExportAdapters2) { irCreateExportAdapter(exportedType, context) }) + } +} + +/** + * Modules are created as object MyModule : KneeModule(varargDependencies, otherStuff) + * We need to intercept the delegating constructor call. + */ +private fun KneeModule.collectDependencies(): List { + var expr: IrExpression? = null + source.constructors.single().body!!.acceptChildrenVoid(object : IrElementVisitorVoid { + override fun visitElement(element: IrElement) { + element.acceptChildrenVoid(this) + } + override fun visitDelegatingConstructorCall(expression: IrDelegatingConstructorCall) { + check(expression.symbol.owner.constructedClass.classId == RuntimeIds.KneeModule) { "Wrong delegating constructor call: ${expression.dumpKotlinLike()}" } + check(expr == null) { "Found two delegating constructor call: ${expr}, ${expression}"} + expr = expression.getValueArgument(0) + super.visitDelegatingConstructorCall(expression) + } + }) + return expr.varargElements().map { it.symbol.owner } +} + +/** + * Initializers are invoked as initKnee(environment, varargDependencies) + * We just need to retrieve and unwrap the second argument.. + */ +private fun KneeInitializer.collectDependencies(): List { + return expression.getValueArgument(1).varargElements().map { it.symbol.owner } +} + +/** + * Vararg expressions can sometimes be null, if no items were provided. + */ +private inline fun IrExpression?.varargElements(): List { + if (this == null) return emptyList() + return (this as IrVararg).elements.map { it as? T ?: error("Vararg elements should be ${T::class}, was ${it::class}") } +} + +private fun KneeCodegen.makeCodegenModule(module: KneeModule, context: KneeContext, exportedTypes: List) { + val name = module.source.name.asString() + val container = prepareContainer(module.source, null) + val moduleClass: ClassName = context.symbols.klass(RuntimeIds.KneeModule).owner.defaultType.asTypeName() as ClassName + val adapterClass: ClassName = moduleClass.nestedClass("Adapter") + val builder = TypeSpec.objectBuilder(name) + .addModifiers(KModifier.PUBLIC) + .let { if (verbose) it.addKdoc("knee:init") else it } + .superclass(context.symbols.klass(RuntimeIds.KneeModule).owner.defaultType.asTypeName()) + .addProperty( + PropertySpec.builder("exportAdapters", MAP.parameterizedBy(INT, adapterClass.parameterizedBy(STAR, STAR)), KModifier.OVERRIDE) + .initializer(CodeBlock.builder() + .addStatement("mapOf(") + .withIndent { + for (exportedType in exportedTypes) { + add("${exportedType.id} to ") + add(CodeBlock.builder().apply { + with(ExportAdapters2) { codegenCreateExportAdapter(exportedType, context) } + }.build()) + add(", ") + } + } + .addStatement(")") + .build() + ) + .build() + ) + container.addChild(CodegenClass(builder)) +} + diff --git a/knee-compiler-plugin/src/main/kotlin/Interfaces.kt b/knee-compiler-plugin/src/main/kotlin/Interfaces.kt new file mode 100644 index 0000000..40e67b2 --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/Interfaces.kt @@ -0,0 +1,316 @@ +package io.deepmedia.tools.knee.plugin.compiler + +import com.squareup.kotlinpoet.* +import io.deepmedia.tools.knee.plugin.compiler.instances.InterfaceNames.asInterfaceName +import io.deepmedia.tools.knee.plugin.compiler.codegen.CodegenClass +import io.deepmedia.tools.knee.plugin.compiler.codegen.CodegenType +import io.deepmedia.tools.knee.plugin.compiler.codegen.KneeCodegen +import io.deepmedia.tools.knee.plugin.compiler.context.KneeContext +import io.deepmedia.tools.knee.plugin.compiler.features.KneeInterface +import io.deepmedia.tools.knee.plugin.compiler.functions.UpwardFunctionSignature +import io.deepmedia.tools.knee.plugin.compiler.jni.JniType +import io.deepmedia.tools.knee.plugin.compiler.codec.CodegenCodecContext +import io.deepmedia.tools.knee.plugin.compiler.codec.IrCodecContext +import io.deepmedia.tools.knee.plugin.compiler.codec.Codec +import io.deepmedia.tools.knee.plugin.compiler.context.KneeOrigin +import io.deepmedia.tools.knee.plugin.compiler.symbols.KneeSymbols +import io.deepmedia.tools.knee.plugin.compiler.export.v1.hasExport1Flag +import io.deepmedia.tools.knee.plugin.compiler.export.v1.ExportAdapters +import io.deepmedia.tools.knee.plugin.compiler.features.KneeUpwardProperty +import io.deepmedia.tools.knee.plugin.compiler.import.ImportInfo +import io.deepmedia.tools.knee.plugin.compiler.import.concrete +import io.deepmedia.tools.knee.plugin.compiler.import.writableParent +import io.deepmedia.tools.knee.plugin.compiler.instances.InstancesCodegen +import io.deepmedia.tools.knee.plugin.compiler.instances.InstancesCodegen.addHandleConstructorAndField +import io.deepmedia.tools.knee.plugin.compiler.instances.InstancesCodegen.addObjectOverrides +import io.deepmedia.tools.knee.plugin.compiler.symbols.CInteropIds +import io.deepmedia.tools.knee.plugin.compiler.utils.* +import io.deepmedia.tools.knee.plugin.compiler.symbols.RuntimeIds.decodeInterface +import io.deepmedia.tools.knee.plugin.compiler.symbols.RuntimeIds.encodeInterface +import io.deepmedia.tools.knee.plugin.compiler.symbols.RuntimeIds.JvmInterfaceWrapper +import org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder +import org.jetbrains.kotlin.descriptors.DescriptorVisibilities +import org.jetbrains.kotlin.descriptors.Modality +import org.jetbrains.kotlin.descriptors.ModuleDescriptor +import org.jetbrains.kotlin.descriptors.findTypeAliasAcrossModuleDependencies +import org.jetbrains.kotlin.ir.builders.* +import org.jetbrains.kotlin.ir.builders.declarations.addConstructor +import org.jetbrains.kotlin.ir.builders.declarations.buildClass +import org.jetbrains.kotlin.ir.declarations.* +import org.jetbrains.kotlin.ir.expressions.IrExpression +import org.jetbrains.kotlin.ir.expressions.impl.IrInstanceInitializerCallImpl +import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol +import org.jetbrains.kotlin.ir.types.typeWith +import org.jetbrains.kotlin.ir.util.* +import org.jetbrains.kotlin.types.KotlinType + +fun preprocessInterface(interface_: KneeInterface, context: KneeContext) { + context.log.logMessage("preprocessInterface(${interface_.source.name}), owned = ${interface_.source.isPartOf(context.module)}") + interface_.makeIrImplementation(context) + context.mapper.register(InterfaceCodec( + context = context, + interfaceClass = interface_.source, + interfaceImplClass = interface_.irImplementation, + importInfo = interface_.importInfo + )) +} + +fun processInterface(interface_: KneeInterface, context: KneeContext, codegen: KneeCodegen, initInfo: InitInfo) { + context.log.logMessage("processInterface(${interface_.source.name}), owned = ${interface_.source.isPartOf(context.module)}") + if (interface_.source.isPartOf(context.module)) { + interface_.makeCodegenClone(codegen) + } + interface_.makeCodegenImplementation(codegen, context) + interface_.makeIrImplementationContents(context) + // Generics should not matter here because we just findClass() the FQN + initInfo.preload(listOf( + interface_.source.defaultType, + interface_.irImplementation.defaultType + )) + + run { + + // Trick so that we don't have to pass the dispatch receiver from the function we are building. + // This is not 100% safe, it would probably fail in nested scopes e.g. inside irLambda. + fun IrBuilderWithScope.irThis() = irGet((scope.scopeOwnerSymbol as IrSimpleFunctionSymbol).owner.dispatchReceiverParameter!!) + + val utilitySuperClass = context.symbols.klass(JvmInterfaceWrapper).owner + val virtualMachine = utilitySuperClass.findDeclaration { it.name.asString() == "virtualMachine" }!!.getter!! + val methodOwner = utilitySuperClass.findDeclaration { it.name.asString() == "methodOwnerClass" }!!.getter!! + val jvmInterfaceObject = utilitySuperClass.findDeclaration { it.name.asString() == "jvmInterfaceObject" }!!.getter!! + val methodFromSignature = utilitySuperClass.findDeclaration { it.name.asString() == "method" }!! + + interface_.irGetVirtualMachine = { irCall(virtualMachine).apply { dispatchReceiver = irThis() }} + interface_.irGetMethodOwner = { irCall(methodOwner).apply { dispatchReceiver = irThis() }} + interface_.irGetJvmObject = { irCall(jvmInterfaceObject).apply { dispatchReceiver = irThis() }} + interface_.irGetMethod = { signature -> irCall(methodFromSignature).apply { + dispatchReceiver = irThis() + putValueArgument(0, irString(signature.jniInfo.name(false).asString() + "::" + signature.jniInfo.signature)) + }} + } + + if (!context.useExport2) { + ExportAdapters.exportIfNeeded(interface_.source, context, codegen, interface_.importInfo) + } +} + +/** + * Given Foo interface, create Foo interface in JVM. + * Note that for local imports with generics, we make the clone generic too, e.g. Flow. + * - That doesn't mean that a generic Flow can cross JNI. The codec still refers to Flow. + * - If user imports Flow and Flow, we don't want to write the codegen clone twice. + * We use addChildIfNeeded for this. + */ +private fun KneeInterface.makeCodegenClone(codegen: KneeCodegen) { + val container = codegen.prepareContainer(source, importInfo) + val builder = when { + source.isFun -> TypeSpec.funInterfaceBuilder(source.name.asString()) + else -> TypeSpec.interfaceBuilder(source.name.asString()) + }.apply { + if (codegen.verbose) addKdoc("knee:interfaces:clone") + addModifiers(source.visibility.asModifier()) + addTypeVariables(importInfo?.typeVariables ?: emptyList()) + } + codegenClone = container.addChildIfNeeded(CodegenClass(builder)).apply { + codegenProducts.add(this) + } +} + +/** + * Given Foo interface, create FooImpl in JVM + * Single constructor accepting the Long stable ref. + */ +private fun KneeInterface.makeCodegenImplementation(codegen: KneeCodegen, context: KneeContext) { + val name = source.codegenName.asInterfaceName(importInfo).asString() + val exported1 = !context.useExport2 && source.hasExport1Flag + val container = codegen.prepareContainer(source, importInfo) + val builder = TypeSpec.classBuilder(name).apply { + when { + exported1 -> addModifiers(KModifier.PUBLIC) + container is CodegenClass && container.isInterface -> {} // Can't put internal inside an interface... + else -> addModifiers(KModifier.INTERNAL) + } + if (codegen.verbose) addKdoc("knee:interfaces:impl") + addSuperinterface(source.defaultType.concrete(importInfo).asTypeName()) + addHandleConstructorAndField(false) + addObjectOverrides(codegen.verbose) + } + codegenImplementation = CodegenClass(builder).apply { + container.addChild(this) + codegenProducts.add(this) + } +} + +/** + * Given Foo interface, create FooImpl in KN + * It should extend the utility class JvmInterfaceWrapper provided by the runtime. + */ +private fun KneeInterface.makeIrImplementation(context: KneeContext) { + val container = source.writableParent(context, importInfo) as IrDeclarationContainer + val sourceConcreteType = source.defaultType.concrete(importInfo) + val superClass = context.symbols.klass(JvmInterfaceWrapper).owner + val wrapperClass = context.factory.buildClass { + this.modality = Modality.FINAL + this.origin = if (importInfo != null) KneeOrigin.KNEE_IMPORT_PARENT else KneeOrigin.KNEE + this.visibility = DescriptorVisibilities.INTERNAL + this.name = source.name.asInterfaceName(importInfo) + }.also { wrapperClass -> + wrapperClass.parent = container + wrapperClass.superTypes = listOf(sourceConcreteType, superClass.typeWith(sourceConcreteType)) + wrapperClass.createParameterDeclarations() // receiver + } + container.addChild(wrapperClass) + irProducts.add(wrapperClass) + irImplementation = wrapperClass +} + +private fun KneeInterface.makeIrImplementationContents(context: KneeContext) { + val sourceConcreteType = source.defaultType.concrete(importInfo) + val superClass = context.symbols.klass(JvmInterfaceWrapper).owner + irImplementation.addConstructor { + this.origin = KneeOrigin.KNEE + this.isPrimary = true + }.let { constructor -> + val superConstructor = superClass.primaryConstructor!! + constructor.valueParameters += superConstructor.valueParameters[0].copyTo(constructor, defaultValue = null) // 0: JniEnvironment + constructor.valueParameters += superConstructor.valueParameters[1].copyTo(constructor, defaultValue = null) // 1: jobject + constructor.body = with(DeclarationIrBuilder(context.plugin, constructor.symbol)) { + irBlockBody { + +irDelegatingConstructorCall(superConstructor).apply { + putValueArgument(0, irGet(constructor.valueParameters[0])) + putValueArgument(1, irGet(constructor.valueParameters[1])) + // Class FQNs will be passed to jni.findClass, so handle dollar sign and codegen renames correctly + putValueArgument(2, irString(CodegenType.from(sourceConcreteType).jvmClassName)) + putValueArgument(3, irString(CodegenType.from(irImplementation.defaultType).jvmClassName)) + // Name and signature of the companion object function, alternated + val allExportedFunctions = upwardFunctions + + upwardProperties.mapNotNull(KneeUpwardProperty::setter) + + upwardProperties.map(KneeUpwardProperty::getter) + putValueArgument(4, irVararg( + elementType = context.symbols.builtIns.stringType, + values = allExportedFunctions.flatMap { + val signature = UpwardFunctionSignature(it.source, it.kind, context.symbols, context.mapper) + listOf( + irString(signature.jniInfo.name(false).asString()), + irString(signature.jniInfo.signature) + ) + } + )) + } + +IrInstanceInitializerCallImpl(startOffset, endOffset, irImplementation.symbol, context.symbols.builtIns.unitType) + } + } + } +} + + +class InterfaceCodec( + private val context: KneeContext, + interfaceClass: IrClass, + private val interfaceImplClass: IrClass, + importInfo: ImportInfo? +) : Codec( + /** + * NOTE: generics (through importInfo) might have a different JVM representation! + * Say, io.deepmedia.knee.buffer.ByteBuffer in native and java.nio.ByteBuffer in JVM + * in the function @Knee fun foo(cb: (ByteBuffer) -> Unit). + * In this case, we are printing the interface with wrong subtypes in JVM codegen. + * + * For now, we fix this by adding type aliases for the buffer case. Not sure about a + * proper solution (CPointer + KneeRaw should have the same problem). + * TODO: Maybe CodegenType.from(localType) should optionally inspect mappers. + */ + localType = interfaceClass.defaultType.concrete(importInfo), + encodedType = JniType.Object(context.symbols, CodegenType.from(ANY)) +) { + + companion object { + fun encodedTypeForFir(module: ModuleDescriptor): KotlinType { + val descr = module.findTypeAliasAcrossModuleDependencies(CInteropIds.COpaquePointer)!! + return descr.expandedType + // return KotlinTypeFactory.simpleNotNullType(TypeAttributes.Empty, descr, emptyList()) + } + + fun encodedTypeForIr(symbols: KneeSymbols): JniType { + return JniType.Object(symbols, CodegenType.from(ANY)) + } + } + + override fun toString() = "InterfaceCodec" + + private val encode = context.symbols.functions(encodeInterface).single() + private val decode = context.symbols.functions(decodeInterface).single() + + /** + * KN: Some interface is going to JVM. + * - if interface was originally JVM, return JVM! + * - otherwise create a KN StableRef and return it as encoded long + */ + override fun IrStatementsBuilder<*>.irEncode(irContext: IrCodecContext, local: IrValueDeclaration): IrExpression { + return irCall(encode).apply { + putTypeArgument(0, localIrType) + putValueArgument(0, irGet(irContext.environment)) + putValueArgument(1, irGet(local)) + } + } + + /** + * KN: Some interface is coming from JVM. + * - if interface is a long, it's a StableRef address + * - otherwise it's a jobject with a reference to a JVM interface. + * In this case we should create a FooImpl instance using the generated impl class. + */ + override fun IrStatementsBuilder<*>.irDecode(irContext: IrCodecContext, jni: IrValueDeclaration): IrExpression { + val logPrefix = "InterfaceCodec(${localCodegenType.name.simpleName})" + irContext.logger.injectLog(this, "$logPrefix DECODING") + return irCall(decode).apply { + putTypeArgument(0, localIrType) + putValueArgument(0, irGet(irContext.environment)) + putValueArgument(1, irGet(jni)) + putValueArgument(2, irLambda( + context = this@InterfaceCodec.context, + parent = parent, + valueParameters = emptyList(), + returnType = interfaceImplClass.defaultType, + content = { + irContext.logger.injectLog(this, "$logPrefix INSTANTIATING the implementation class") + +irReturn(irCallConstructor(interfaceImplClass.primaryConstructor!!.symbol, emptyList()).apply { + putValueArgument(0, irGet(irContext.environment)) // environment + putValueArgument(1, irGet(jni)) // jobject + }) + } + )) + } + } + + /** + * JVM: some interface implementation arrived from KN in form of kotlin.Any. It could be + * - A boxed java.lang.Long pointing to a stable ref address, which can be used for delegation + * In this case we should create an instance of "KneeFoo" passing the address to the constructor + * - An actual interface. This happens if the interface was originally created in Java. + */ + override fun CodeBlock.Builder.codegenDecode(codegenContext: CodegenCodecContext, jni: String): String { + val fqn = localCodegenType.name + val impl = interfaceImplClass.defaultType.asTypeName() + /* val impl = fqn + .copy(simpleName = fqn.simpleName.asInterfaceName(importInfo)) + .copy(clearGenerics = true) + .copy(packageName = remapBasedOnWritablePackage()) */ + addStatement("val ${jni}_: %T =", fqn) + withIndent { + addStatement("if ($jni is %T) $jni as %T", fqn.copy(wildcardGenerics = true), fqn) + addStatement("else %T($jni as Long)", impl) + } + return "${jni}_" + } + + override fun CodeBlock.Builder.codegenEncode(codegenContext: CodegenCodecContext, local: String): String { + // Special case during JVM to KN functions when the interface is the receiver. + // It's not fundamental but avoids some warnings in generated code (this is Type where this is obviously type) + if (local == "this") return "$local.`${InstancesCodegen.HandleField}`" + + val impl = interfaceImplClass.defaultType.asTypeName() + addStatement("val ${local}_: Any = ($local as? %T)?.`${InstancesCodegen.HandleField}` ?: $local", impl) + return "${local}_" + } +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/MainBir.kt b/knee-compiler-plugin/src/main/kotlin/MainBir.kt new file mode 100644 index 0000000..c5ef1b7 --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/MainBir.kt @@ -0,0 +1,97 @@ +package io.deepmedia.tools.knee.plugin.compiler + +import io.deepmedia.tools.knee.plugin.compiler.codegen.KneeCodegen +import io.deepmedia.tools.knee.plugin.compiler.context.KneeContext +import io.deepmedia.tools.knee.plugin.compiler.features.KneeCollector +import io.deepmedia.tools.knee.plugin.compiler.features.KneeFeature +import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension +import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext +import org.jetbrains.kotlin.cli.common.messages.MessageCollector +import org.jetbrains.kotlin.ir.declarations.IrModuleFragment +import org.jetbrains.kotlin.ir.util.classId +import java.io.File + +class KneeIrGeneration( + private val logs: MessageCollector, + private val verboseLogs: Boolean, + private val verboseRuntime: Boolean, + private val verboseCodegen: Boolean, + private val outputDir: File, + private val useExport2: Boolean, +) : IrGenerationExtension { + override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) { + val context = KneeContext(pluginContext, logs, verboseLogs, verboseRuntime, moduleFragment, useExport2) + val codegen = KneeCodegen(context, outputDir, verboseCodegen) + process(context, codegen) + } +} + +private fun process(context: KneeContext, codegen: KneeCodegen) { + val unit = "${context.module.name} (${context.module.descriptor.platform})" + context.log.logMessage("[*] START unit: $unit") + val data = KneeCollector(context.module) + context.log.logMessage("[*] Collected") + + val hasData = data.hasDeclarations + if (data.initializers.isEmpty() && data.modules.isEmpty()) { + if (hasData) error("Compilation unit $unit should either initialize Knee with `initKnee()` or create a KneeModule top-level property, exposed to dependent modules.") + else return // all empty + } + if (data.initializers.isNotEmpty() && data.modules.isNotEmpty()) { + error("Compilation unit $unit should either initialize Knee with `initKnee()` or create a KneeModule top-level property. Currently doing both.") + } + if (data.modules.size > 1) { + context.log.logWarning("Compilation unit $unit has ${data.modules.size} modules: ${data.modules}") + } + context.log.logMessage("[*] Initializers: ${data.initializers.size} Modules: ${data.modules.size}") + val initInfo = when { + data.initializers.isNotEmpty() -> InitInfo.Initializer(data.initializers) + data.modules.isNotEmpty() -> InitInfo.Module(data.modules) + else -> error("Can't happen: ${data.initializers}, ${data.modules}") + } + context.log.logMessage("[*] Dependencies: ${context.mapper.dependencies.size} ${context.mapper.dependencies.map { it.key.classId }}") + context.mapper.dependencies = initInfo.dependencies(context.json) + + // Moved export() to KneeModule so of course one can't ever export without a module + /* if (context.useExport2 && !initInfo.canExport2Declarations) { + val exported = (data.allClasses + data.allInterfaces + data.allEnums).filter { it.source.hasExportFlg } + if (exported.isNotEmpty()) { + error("Compilation unit $unit uses initKnee, not KneeModule(). As such, it can't export declarations " + + "to consumer libraries. Please remove the exported flag from ${exported.size} declarations: $exported") + } + }*/ + + // Preprocessing round is meant for features to add codecs so that there can be circular dependencies between types + context.log.logMessage("[*] Preprocessing target:${context.module.name} platform:${context.plugin.platform}") + data.allInterfaces.processEach(context) { preprocessInterface(it, context) } + data.allClasses.processEach(context) { preprocessClass(it, context) } + + context.log.logMessage("[*] Processing target:${context.module.name} platform:${context.plugin.platform}") + data.allEnums.processEach(context) { processEnum(it, context, codegen) } + data.allClasses.processEach(context) { processClass(it, context, codegen, initInfo) } + data.allInterfaces.processEach(context) { processInterface(it, context, codegen, initInfo) } + data.allUpwardProperties.processEach(context) { processUpwardProperty(it, context) } + data.allDownwardProperties.processEach(context) { processDownwardProperty(it, context, codegen) } + data.allUpwardFunctions.processEach(context) { processUpwardFunction(it, context, codegen) } + data.allDownwardFunctions.processEach(context) { processDownwardFunction(it, context, codegen, initInfo) } + + processInit(info = initInfo, context = context, codegen = codegen) + context.log.logMessage("[*] Writing generated code in ${codegen.root.absolutePath}") + codegen.write() + + /* val exportedData = (data.allEnums + data.allInterfaces + data.allClasses).joinToString { + it.source.defaultType.toString() + } + context.log.print("[*] Exporting data: $exportedData") */ + +} + +private inline fun > List.processEach(context: KneeContext, block: (T) -> Unit) { + forEach { it.process(context, block) } +} + +private inline fun > T.process(context: KneeContext, block: (T) -> Unit) { + context.log.logMessage("[*] Processing $this:\n${this.dump(rawIr = false)}") + block(this) + context.log.logMessage("[*] Processed $this:\n${this.dump(rawIr = false)}") +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/MainFir.kt b/knee-compiler-plugin/src/main/kotlin/MainFir.kt new file mode 100644 index 0000000..ebde85a --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/MainFir.kt @@ -0,0 +1,90 @@ +package io.deepmedia.tools.knee.plugin.compiler + +import io.deepmedia.tools.knee.plugin.compiler.export.v1.hasExport1Flag +import io.deepmedia.tools.knee.plugin.compiler.export.v1.ExportFirDescriptors +import io.deepmedia.tools.knee.plugin.compiler.export.v1.ExportInfo +import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.descriptors.SimpleFunctionDescriptor +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.extensions.SyntheticResolveExtension +import org.jetbrains.kotlin.resolve.lazy.LazyClassContext +import org.jetbrains.kotlin.resolve.lazy.declarations.ClassMemberDeclarationProvider + +/** + * Needed for K1 exports, unused now. + */ +class KneeSyntheticResolve : SyntheticResolveExtension { + private val exportFirCache = mutableMapOf() + + private fun getExportFirOfAdapter(adapter: ClassDescriptor): ExportFirDescriptors? { + return exportFirCache.values.firstOrNull { it?.adapterDescriptor == adapter } + } + + private fun getExportFirOfClass(exportedClass: ClassDescriptor): ExportFirDescriptors? { + return exportFirCache.getOrPut(exportedClass) { + if (!exportedClass.hasExport1Flag) return@getOrPut null + ExportFirDescriptors(exportedClass) + } + } + + + override fun getSyntheticFunctionNames(thisDescriptor: ClassDescriptor): List { + var exportDescriptor = getExportFirOfClass(thisDescriptor) + if (exportDescriptor != null) { + return listOf(exportDescriptor.annotatedFunctionName) + } + exportDescriptor = getExportFirOfAdapter(thisDescriptor) + if (exportDescriptor != null) { + return exportDescriptor.adapterFunctionNames + } + return super.getSyntheticFunctionNames(thisDescriptor) + } + + override fun getSyntheticNestedClassNames(thisDescriptor: ClassDescriptor): List { + val exportDescriptor = getExportFirOfClass(thisDescriptor) + return when (val location = exportDescriptor?.exportInfo?.adapterNativeCoordinates) { + null -> super.getSyntheticNestedClassNames(thisDescriptor) + is ExportInfo.NativeCoordinates.InnerObject -> listOf(location.name) + } + } + + override fun generateSyntheticMethods( + thisDescriptor: ClassDescriptor, + name: Name, + bindingContext: BindingContext, + fromSupertypes: List, + result: MutableCollection + ) { + var exportDescriptor = getExportFirOfClass(thisDescriptor) + if (exportDescriptor?.annotatedFunctionName == name) { + result.add(exportDescriptor.makeAnnotatedFunctionDescriptor()) + return + } + exportDescriptor = getExportFirOfAdapter(thisDescriptor) + if (exportDescriptor != null && name in exportDescriptor.adapterFunctionNames) { + result.add(exportDescriptor.makeAdapterFunctionDescriptor(thisDescriptor, name)) + return + } + super.generateSyntheticMethods(thisDescriptor, name, bindingContext, fromSupertypes, result) + } + + override fun generateSyntheticClasses( + thisDescriptor: ClassDescriptor, + name: Name, + ctx: LazyClassContext, + declarationProvider: ClassMemberDeclarationProvider, + result: MutableSet + ) { + val exportDescriptor = getExportFirOfClass(thisDescriptor) + when (val location = exportDescriptor?.exportInfo?.adapterNativeCoordinates) { + null -> {} + is ExportInfo.NativeCoordinates.InnerObject -> { + if (location.name == name) { + result.add(exportDescriptor.makeAdapterDescriptor(ctx, declarationProvider, name)) + } + } + } + super.generateSyntheticClasses(thisDescriptor, name, ctx, declarationProvider, result) + } +} diff --git a/knee-compiler-plugin/src/main/kotlin/UpwardFunctions.kt b/knee-compiler-plugin/src/main/kotlin/UpwardFunctions.kt new file mode 100644 index 0000000..6ad1e13 --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/UpwardFunctions.kt @@ -0,0 +1,201 @@ +package io.deepmedia.tools.knee.plugin.compiler + +import com.squareup.kotlinpoet.* +import io.deepmedia.tools.knee.plugin.compiler.codegen.CodegenFunction +import io.deepmedia.tools.knee.plugin.compiler.codegen.KneeCodegen +import io.deepmedia.tools.knee.plugin.compiler.context.KneeContext +import io.deepmedia.tools.knee.plugin.compiler.features.KneeUpwardFunction +import io.deepmedia.tools.knee.plugin.compiler.features.KneeInterface +import io.deepmedia.tools.knee.plugin.compiler.functions.* +import io.deepmedia.tools.knee.plugin.compiler.codec.CodegenCodecContext +import io.deepmedia.tools.knee.plugin.compiler.codec.IrCodecContext +import io.deepmedia.tools.knee.plugin.compiler.context.KneeLogger +import io.deepmedia.tools.knee.plugin.compiler.utils.* +import io.deepmedia.tools.knee.plugin.compiler.symbols.CInteropIds +import io.deepmedia.tools.knee.plugin.compiler.symbols.PlatformIds +import io.deepmedia.tools.knee.plugin.compiler.symbols.RuntimeIds.kneeInvokeKnSuspend +import io.deepmedia.tools.knee.plugin.compiler.symbols.RuntimeIds.useEnv +import org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder +import org.jetbrains.kotlin.descriptors.Modality +import org.jetbrains.kotlin.ir.builders.* +import org.jetbrains.kotlin.ir.builders.declarations.addFunction +import org.jetbrains.kotlin.ir.builders.declarations.addValueParameter +import org.jetbrains.kotlin.ir.declarations.* +import org.jetbrains.kotlin.ir.types.typeWith +import org.jetbrains.kotlin.ir.util.* + +fun processUpwardFunction( + function: KneeUpwardFunction, + context: KneeContext, + codegen: KneeCodegen +) { + val signature = UpwardFunctionSignature(function.source, function.kind, context.symbols, context.mapper) + val implementation = function.makeIr(context, signature) + function.makeCodegen(codegen, signature, implementation, context.log) +} + +/** + * We put the function inside the companion object of the class where the reverse function was implemented. + * Such class is returned by [KneeInterface.irImplementation] - for interfaces, it's "Knee${interface}". + * + * Note that we use the companion object of it, we want the function to be @JvmStatic. + * Another option was using the companion object of the interface itself, but there's a rule in Kotlin where + * static members of the companion object of an interface must be public, which is unacceptable for us. + * + * So we just use [KneeInterface.irImplementation] since it already exists. + * It's tricky because the codegen version of [KneeInterface.irImplementation] is actually used by + * regular functions (not reverse), but since we use the companion object there's no overlap. + */ +private fun KneeUpwardFunction.makeCodegen( + codegen: KneeCodegen, + signature: UpwardFunctionSignature, + implementation: IrSimpleFunction, + logger: KneeLogger +) { + val spec = FunSpec + .builder(signature.jniInfo.name(includeAncestors = false).asString()) + .addModifiers(KModifier.PRIVATE) + .addAnnotation(ClassName.bestGuess("kotlin.jvm.JvmStatic")) + .returns((if (signature.isSuspend) signature.suspendResult else signature.result).encodedType.jvmOrNull?.name ?: UNIT) + + // Parameters + signature.extraParameters.forEach { (param, codec) -> + val name = param.asStringSafeForCodegen(true) + spec.addParameter(name, codec.encodedType.jvmOrNull!!.name) + } + signature.regularParameters.forEach { (param, codec) -> + val name = param.asStringSafeForCodegen(true) + spec.addParameter(name, codec.encodedType.jvmOrNull!!.name) + } + + // Code block + with(UpwardFunctionsCodegen) { + // The receiver should be received as itself (no long tricks) and needs no mapping + val codecContext = CodegenCodecContext(source.symbol, true, logger) + if (!signature.isSuspend) { + spec.addCode(CodeBlock.builder().apply { + codegenInvoke(signature, "val res = ", codecContext) + codegenReceive("res", signature, "return ", codecContext) + }.build()) + } else { + // Function has two prefixes - receiver and then suspendInvoker passed as a long. + // The function should return a SuspendInvocation object. Helper signature: + // fun kneeInvokeKnSuspend(invoker: Long, block: suspend () -> T): KneeSuspendInvocation + spec.addCode(CodeBlock.builder().apply { + val invoke = MemberName("io.deepmedia.tools.knee.runtime.compiler", "kneeInvokeKnSuspend") + beginControlFlow("return %M<%T>(${UpwardFunctionSignature.Extra.SuspendInvoker}) {", invoke, signature.result.encodedType.jvmOrNull?.name ?: UNIT) + codegenInvoke(signature, "val res = ", codecContext) + codegenReceive("res", signature, "", codecContext) + endControlFlow() + }.build()) + } + } + + // Save + if (codegen.verbose) spec.addKdoc("knee:reverse-functions") + val product = CodegenFunction(spec) + codegen.prepareContainer( + declaration = implementation, + importInfo = kind.importInfo, + detectPropertyAccessors = false, // we don't generate properties at all in the companion object + createCompanionObject = true + ).addChild(product) + codegenProducts.add(product) +} + +private fun KneeUpwardFunction.makeIr(context: KneeContext, signature: UpwardFunctionSignature): IrSimpleFunction { + val envType = context.symbols.klass(CInteropIds.CPointer) + .typeWith(context.symbols.typeAliasUnwrapped(PlatformIds.JNIEnvVar)) + + val kind = kind as KneeUpwardFunction.Kind.InterfaceMember + + // reuse function if it exists already. this happens in the case of reverse properties + // where we prefer to add getter / setter there to properly configure them + val implementation = implementation ?: kind.parent.irImplementation.addFunction { + name = source.name + isSuspend = source.isSuspend + modality = Modality.FINAL + origin = source.origin + // Without this, suspend function generation fails! + startOffset = SYNTHETIC_OFFSET + endOffset = SYNTHETIC_OFFSET + }.also { this.implementation = it } + + return implementation.apply { + // Configure return type. Not source.returnType, that will fail for generics + returnType = signature.result.localIrType + + // Configure value parameters. First option is 'copyParameterDeclarationsFrom(source)' + // but that copies type parameters too, fails for generics. We have concrete types. + // Use the import susbstitution map instead, or TODO: use signature value parameters + copyValueParametersFrom(source, kind.importInfo?.substitutionMap ?: emptyMap()) + + // This function overrides the source function + // Could also += source.overriddenSymbols, not sure if needed, we're not doing it elsewhere + overriddenSymbols += source.symbol + + val logPrefix = "ReverseFunctions.kt(${source.fqNameWhenAvailable})" + body = DeclarationIrBuilder(context.plugin, symbol, SYNTHETIC_OFFSET, SYNTHETIC_OFFSET).irBlockBody { + context.log.injectLog(this, "$logPrefix INVOKED, retrieving jvm info") + val jvmMethodOwner = irTemporary(kind.parent.irGetMethodOwner(this)) + val jvmMethod = irTemporary(kind.parent.irGetMethod(this, signature)) + val jvmObject = irTemporary(kind.parent.irGetJvmObject(this)) + val args = valueParameters + if (!signature.isSuspend) { + +irReturn(irCall( + callee = context.symbols.functions(useEnv).single() + ).apply { + extensionReceiver = kind.parent.irGetVirtualMachine(this@irBlockBody) + putTypeArgument(0, signature.result.localIrType) + putValueArgument(0, irLambda( + context = context, + parent = parent, + content = { lambda -> + lambda.returnType = signature.result.localIrType + val env = lambda.addValueParameter("_env", envType) + + context.log.injectLog(this, "$logPrefix got environment, preparing the JVM call") + val codecContext = IrCodecContext(source.symbol, env, true, context.log) + with(UpwardFunctionsIr) { + val raw = irInvoke(context.symbols, args, signature, codecContext, jvmObject, jvmMethodOwner, jvmMethod, signature.result.encodedType) + +irReturn(irReceive(raw, signature, codecContext)) + } + } + )) + }) + } else { + // See kneeInvokeKnSuspend signature in runtime + context.log.injectLog(this, "$logPrefix suspend machinery started") + +irReturn(irCall(context.symbols.functions(kneeInvokeKnSuspend).single()).apply { + putTypeArgument(0, signature.result.encodedType.knOrNull ?: context.symbols.builtIns.unitType) + putTypeArgument(1, signature.result.localIrType) + putValueArgument(0, kind.parent.irGetVirtualMachine(this@irBlockBody)) + putValueArgument(1, irLambda(context, parent) { lambda -> + val env = lambda.addValueParameter("_env", envType) + val invoker = lambda.addValueParameter("_invoker", context.symbols.builtIns.longType) + lambda.returnType = signature.suspendResult.localIrType + with(UpwardFunctionsIr) { + val codecContext = IrCodecContext(source.symbol, env, true, context.log) + context.log.injectLog(this@irBlockBody, "$logPrefix preparing the JVM call") + val raw = irInvoke(context.symbols, args, signature, codecContext, jvmObject, jvmMethodOwner, jvmMethod, signature.suspendResult.encodedType, invoker) + context.log.injectLog(this@irBlockBody, "$logPrefix received the invocation token") + +irReturn(irReceive(raw, signature, codecContext, suspendToken = true)) + } + }) + putValueArgument(2, irLambda(context, parent) { lambda -> + val env = lambda.addValueParameter("_env", envType) + val raw = lambda.addValueParameter("_result", signature.result.encodedType.knOrNull ?: context.symbols.builtIns.unitType) + lambda.returnType = signature.result.localIrType + with(UpwardFunctionsIr) { + val codecContext = IrCodecContext(source.symbol, env, true, context.log) + context.log.injectLog(this@irBlockBody, "$logPrefix received the suspend function result. unwrapping it") + +irReturn(irReceive(irGet(raw), signature, codecContext)) + } + }) + }) + } + } + }.also { + irProducts.add(it) + } +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/UpwardProperties.kt b/knee-compiler-plugin/src/main/kotlin/UpwardProperties.kt new file mode 100644 index 0000000..13a2f47 --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/UpwardProperties.kt @@ -0,0 +1,50 @@ +package io.deepmedia.tools.knee.plugin.compiler + +import io.deepmedia.tools.knee.plugin.compiler.context.KneeContext +import io.deepmedia.tools.knee.plugin.compiler.features.KneeUpwardProperty +import org.jetbrains.kotlin.descriptors.Modality +import org.jetbrains.kotlin.ir.builders.declarations.* +import org.jetbrains.kotlin.ir.declarations.IrProperty +import org.jetbrains.kotlin.name.Name + +fun processUpwardProperty(property: KneeUpwardProperty, context: KneeContext) { + property.makeIr(context) +} + +private fun KneeUpwardProperty.makeIr(context: KneeContext): IrProperty { + val implementationClass = kind.parent.irImplementation + return implementationClass.addProperty { + this.name = source.name + this.origin = source.origin + this.modality = Modality.FINAL + this.isVar = source.isVar + }.also { implementation -> + implementation.overriddenSymbols += source.symbol + val propertyType = getter.source.returnType + // backing field: there's none, getter and setter delegate to JVM. + // setter and getter: we add blank ones here, then body is added by ReverseFunctions.kt + getter.let { knee -> + knee.implementation = implementation.addGetter { + this.returnType = propertyType + }.apply { + // Removing, handled in ReverseFunctions.kt + // dispatchReceiverParameter = implementationClass.thisReceiver!!.copyTo(this) + } + } + setter?.let { knee -> + knee.implementation = implementation.factory.buildFun { + this.returnType = context.symbols.builtIns.unitType + this.name = Name.special("") + }.apply { + implementation.setter = this + correspondingPropertySymbol = implementation.symbol + parent = implementation.parent + // Removing, handled in ReverseFunctions.kt + // dispatchReceiverParameter = implementationClass.thisReceiver!!.copyTo(this) + // addValueParameter("value", propertyType) + } + } + }.also { + irProducts.add(it) + } +} diff --git a/knee-compiler-plugin/src/main/kotlin/codec/BufferCodecs.kt b/knee-compiler-plugin/src/main/kotlin/codec/BufferCodecs.kt new file mode 100644 index 0000000..629bc07 --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/codec/BufferCodecs.kt @@ -0,0 +1,78 @@ +package io.deepmedia.tools.knee.plugin.compiler.codec + +import com.squareup.kotlinpoet.CodeBlock +import io.deepmedia.tools.knee.plugin.compiler.codegen.CodegenType +import io.deepmedia.tools.knee.plugin.compiler.symbols.KneeSymbols +import io.deepmedia.tools.knee.plugin.compiler.jni.JniType +import io.deepmedia.tools.knee.plugin.compiler.symbols.JDKIds +import io.deepmedia.tools.knee.plugin.compiler.symbols.PlatformIds +import io.deepmedia.tools.knee.plugin.compiler.symbols.RuntimeIds.PrimitiveBuffer +import io.deepmedia.tools.knee.plugin.compiler.utils.simple +import org.jetbrains.kotlin.ir.builders.IrStatementsBuilder +import org.jetbrains.kotlin.ir.builders.irCall +import org.jetbrains.kotlin.ir.builders.irCallConstructor +import org.jetbrains.kotlin.ir.builders.irGet +import org.jetbrains.kotlin.ir.declarations.IrValueDeclaration +import org.jetbrains.kotlin.ir.expressions.IrExpression +import org.jetbrains.kotlin.ir.types.IrSimpleType +import org.jetbrains.kotlin.ir.types.classOrNull +import org.jetbrains.kotlin.ir.types.defaultType +import org.jetbrains.kotlin.ir.util.constructors +import org.jetbrains.kotlin.ir.util.getPropertyGetter + + +fun bufferCodecs(symbols: KneeSymbols) = listOf( + BufferCodec(symbols, "Byte"), + BufferCodec(symbols, "Int"), + BufferCodec(symbols, "Long"), + BufferCodec(symbols, "Float"), + BufferCodec(symbols, "Double") +) + +private class BufferCodec( + symbols: KneeSymbols, + runtimeType: IrSimpleType, + jdkType: CodegenType, + private val dataType: String +): Codec( + localIrType = runtimeType, + localCodegenType = jdkType, + encodedType = JniType.Object(symbols, jdkType) +) { + + override fun toString(): String { + return "BufferCodec($dataType)" + } + + private val objGetter = localIrType.classOrNull!!.getPropertyGetter("obj")!! + private val createBuffer = localIrType.classOrNull!!.constructors.single { + val params = it.owner.valueParameters + params.size == 2 && params[1].type == symbols.typeAliasUnwrapped(PlatformIds.jobject) + } + + constructor(symbols: KneeSymbols, dataType: String) : this( + symbols = symbols, + runtimeType = symbols.klass(PrimitiveBuffer(dataType)).defaultType.simple("BufferCodecs.init"), + jdkType = CodegenType.from(JDKIds.NioBuffer(dataType)), + dataType = dataType + ) + + override fun IrStatementsBuilder<*>.irDecode(irContext: IrCodecContext, jni: IrValueDeclaration): IrExpression { + return irCallConstructor(createBuffer, emptyList()).apply { + putValueArgument(0, irGet(irContext.environment)) + putValueArgument(1, irGet(jni)) + } + } + + override fun IrStatementsBuilder<*>.irEncode(irContext: IrCodecContext, local: IrValueDeclaration): IrExpression { + return irCall(objGetter).apply { dispatchReceiver = irGet(local) } + } + + override fun CodeBlock.Builder.codegenEncode(codegenContext: CodegenCodecContext, local: String): String { + return local + } + + override fun CodeBlock.Builder.codegenDecode(codegenContext: CodegenCodecContext, jni: String): String { + return jni + } +} diff --git a/knee-compiler-plugin/src/main/kotlin/codec/Codec.kt b/knee-compiler-plugin/src/main/kotlin/codec/Codec.kt new file mode 100644 index 0000000..e1b7ad6 --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/codec/Codec.kt @@ -0,0 +1,77 @@ +package io.deepmedia.tools.knee.plugin.compiler.codec + +import com.squareup.kotlinpoet.CodeBlock +import io.deepmedia.tools.knee.plugin.compiler.codegen.CodegenType +import io.deepmedia.tools.knee.plugin.compiler.context.KneeLogger +import io.deepmedia.tools.knee.plugin.compiler.jni.JniType +import org.jetbrains.kotlin.ir.builders.IrStatementsBuilder +import org.jetbrains.kotlin.ir.declarations.IrValueDeclaration +import org.jetbrains.kotlin.ir.expressions.IrExpression +import org.jetbrains.kotlin.ir.symbols.IrFunctionSymbol +import org.jetbrains.kotlin.ir.types.IrSimpleType + +abstract class Codec( + val localIrType: IrSimpleType, + val localCodegenType: CodegenType, + val encodedType: JniType +) { + + // Most of the times the local types are identical so this constructor can be used. + constructor(localType: IrSimpleType, encodedType: JniType) : this(localType, CodegenType.from(localType), encodedType) + + /** + * Whether [irDecode] and [irEncode] should be called for backend side conversion. + * By default, checks type equality but it is open to be overridden for special cases (e.g. one might + * map Int to Int but still do something in between). + */ + open val needsIrConversion: Boolean = encodedType !is JniType.Real || encodedType.kn != localIrType + + /** + * Whether [codegenDecode] and [codegenEncode] should be called for frontend side conversion. + * By default, checks type equality but it is open to be overridden for special cases (e.g. one might + * map Int to Int but still do something in between). + */ + open val needsCodegenConversion: Boolean = encodedType !is JniType.Real || encodedType.jvm != localCodegenType + + /** + * Used before wrapping this codec in some other codec which needs encoded type of [JniType.Real]. + * Used for generics, nullable types and so on. This function exists in order to be overridden + * by [ReturnVoidCodec], which should use [UnitCodec] when wrapped instead of itself. + * + * The alternative was to use UnitCodec by default but make sure that [ReturnVoidCodec] is used + * in case of basic return types. That road is harder due to suspend support, not knowing if some type is + * going to be used in both ways, ... + */ + open fun wrappable(): Codec = this + + abstract fun IrStatementsBuilder<*>.irDecode(irContext: IrCodecContext, jni: IrValueDeclaration): IrExpression + abstract fun IrStatementsBuilder<*>.irEncode(irContext: IrCodecContext, local: IrValueDeclaration): IrExpression + + abstract fun CodeBlock.Builder.codegenDecode(codegenContext: CodegenCodecContext, jni: String): String + abstract fun CodeBlock.Builder.codegenEncode(codegenContext: CodegenCodecContext, local: String): String +} + +data class IrCodecContext( + val functionSymbol: IrFunctionSymbol?, + val environment: IrValueDeclaration, + // regular context decodes/reads the parameters and encodes/writes the return type + // reverse context decodes/reads the return type and encodes/writes the parameters + val reverse: Boolean, + val logger: KneeLogger +) { + val encodesParameters get() = reverse + val encodesReturn get() = !encodesParameters + val decodesParameters get() = !encodesParameters + val decodesReturn get() = !encodesReturn +} + +data class CodegenCodecContext( + val functionSymbol: IrFunctionSymbol?, + val reverse: Boolean, + val logger: KneeLogger +) { + val encodesParameters get() = !reverse // T + val encodesReturn get() = !encodesParameters // F + val decodesParameters get() = !encodesParameters // F + val decodesReturn get() = !encodesReturn // T +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/codec/CollectionCodec.kt b/knee-compiler-plugin/src/main/kotlin/codec/CollectionCodec.kt new file mode 100644 index 0000000..23c127a --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/codec/CollectionCodec.kt @@ -0,0 +1,243 @@ +package io.deepmedia.tools.knee.plugin.compiler.codec + +import com.squareup.kotlinpoet.* +import io.deepmedia.tools.knee.plugin.compiler.context.KneeContext +import io.deepmedia.tools.knee.plugin.compiler.jni.JniType +import io.deepmedia.tools.knee.plugin.compiler.symbols.RuntimeIds.JObjectCollectionCodec +import io.deepmedia.tools.knee.plugin.compiler.symbols.RuntimeIds.PrimitiveArraySpec +import io.deepmedia.tools.knee.plugin.compiler.symbols.RuntimeIds.PrimitiveCollectionCodec +import io.deepmedia.tools.knee.plugin.compiler.symbols.RuntimeIds.TransformingCollectionCodec +import io.deepmedia.tools.knee.plugin.compiler.symbols.RuntimeIds.typedArraySpec +import io.deepmedia.tools.knee.plugin.compiler.utils.irLambda +import org.jetbrains.kotlin.ir.builders.* +import org.jetbrains.kotlin.ir.declarations.IrClass +import org.jetbrains.kotlin.ir.declarations.IrValueDeclaration +import org.jetbrains.kotlin.ir.expressions.IrDeclarationReference +import org.jetbrains.kotlin.ir.expressions.IrExpression +import org.jetbrains.kotlin.ir.util.functions +import org.jetbrains.kotlin.ir.util.primaryConstructor + +fun Codec.withCollectionCodecs( + context: KneeContext, + vararg kinds: CollectionKind = CollectionKind.entries.toTypedArray() +): Array = listOf( + this, *collectionCodecs(context, *kinds) +).toTypedArray() + +fun Codec.collectionCodecs( + context: KneeContext, + vararg kinds: CollectionKind = CollectionKind.entries.toTypedArray() +): Array { + return kinds.map { kind -> CollectionCodec(context, this.wrappable(), kind) }.toTypedArray() +} + +/** + * A pretty complex codec that wraps an element codec to provide collection support. + * We support different kinds of collections, see [CollectionKind]. + * + * For example, given a [StringCodec] which has: + * - local ir type is kotlin.String [StringCodec.localIrType] + * - jni type is jobject <-> kotlin.String [StringCodec.encodedType] + * - local codegen type is kotlin.String [StringCodec.localCodegenType] + * + * First of all, the jni representation of a collection of strings is [JniType.Array]. + * This is determined automatically by [JniType.Object.array]. + * + * Then the mapper must decode a jobjectArray into a List/Set/Array/Sequence of strings. This + * is done by leveraging runtime utilities called codecs. Codec expose functions with the List/Set/Array/Sequence + * name in it, which we can fetch at compile time here in the plugin. + * + * By default, codecs respect the inner type, so a jobjectArray can become a List, Set and so on. + * This is not what we want because the element codec might be mapping between different types, + * like in our example jobject <==> kotlin.String . + * + * For this reason, a special codec called TransformingCollectionCodec exists which takes two lambdas for + * encoding and decoding the object. This codec will implement the lambdas by delegating them + * to the wrapped codec, so that jobject is transformed to kotlin.String and viceversa. + */ + +// TODO: revisit - when elementCodec does transform, we create a new instance of transforming helper at every encode decode! +// TODO: also for a function say foo(List): List, we create it twice, one for the param and one for return +// TODO: wrap in KneeMapper instead of using withCollectionCodecs() +class CollectionCodec constructor( + private val context: KneeContext, + private val elementCodec: Codec, + private val collectionKind: CollectionKind +) : Codec( + localIrType = collectionKind.getCollectionTypeOf(elementCodec.localIrType, context.symbols), + localCodegenType = collectionKind.getCollectionTypeOf(elementCodec.localCodegenType, context.symbols), + encodedType = when (val type = elementCodec.encodedType) { + is JniType.Primitive -> type.array(context.symbols) + is JniType.Object -> type.array(context.symbols) + else -> error("Unsupported element type: $type") + } +) { + /** + * The inner codec is the one that transforms the jobjectArray in a Collection. + * We have two different implementations based on whether the encoded type is a primitive or not. + */ + private val runtimeHelperClassRaw: IrClass = when (val type = elementCodec.encodedType) { + is JniType.Primitive -> context.symbols.klass(PrimitiveCollectionCodec(type.knSimpleName)).owner + is JniType.Object -> context.symbols.klass(JObjectCollectionCodec).owner + else -> error("Not possible") + } + + /** + * The outer codec wraps the [runtimeHelperClassRaw] (if needed) to transform the inner element type. + * For example, it will transform a Collection into a Collection. + */ + private val runtimeHelperClass: IrClass = when { + elementCodec.needsIrConversion -> context.symbols.klass(TransformingCollectionCodec).owner + else -> runtimeHelperClassRaw + } + + private fun IrBuilderWithScope.irGetOrCreateHelperRaw(): IrDeclarationReference { + return when (val type = elementCodec.encodedType) { + is JniType.Primitive -> irGetObject(runtimeHelperClassRaw.symbol) + is JniType.Object -> irCallConstructor(runtimeHelperClassRaw.primaryConstructor!!.symbol, emptyList()).apply { + putValueArgument(0, irString(type.jvm.jvmClassName)) + } + else -> error("Should not happen") + } + } + + private fun IrBuilderWithScope.irGetOrCreateHelper(codecContext: IrCodecContext): IrDeclarationReference { + if (!elementCodec.needsIrConversion) return irGetOrCreateHelperRaw() + + // We're creating a transforming codec. + val rawHelper = irGetOrCreateHelperRaw() + return irCallConstructor(runtimeHelperClass.primaryConstructor!!.symbol, listOf( + // Type arguments: Source (e.g. jobject), Transformed (e.g. String), TransformedArrayType (e.g. Array) + elementCodec.encodedType.knOrNull!!, + elementCodec.localIrType, + CollectionKind.Array.getCollectionTypeOf(elementCodec.localIrType, this@CollectionCodec.context.symbols) + )).apply { + // Constructor param: CollectionCodec + putValueArgument(0, rawHelper) + // Constructor param: ArraySpec + // Return type of this is symbols.klass(runtimeArraySpecClass) + // .typeWith(CollectionKind.Array.getCollectionType(elementCodec.localType, symbols), elementCodec.localType) + putValueArgument(1, when (val type = elementCodec.encodedType) { + is JniType.Primitive -> { + val name = PrimitiveArraySpec(type.jvmSimpleName) + irGetObject(this@CollectionCodec.context.symbols.klass(name)) + } + is JniType.Object -> { + val name = typedArraySpec + irCall(this@CollectionCodec.context.symbols.functions(name).single()).apply { + putTypeArgument(0, type.kn) + } + } + else -> error("Not possible") + }) + // Constructor param: Source --> Transformed decoding lambda + putValueArgument(2, irLambda( + context = this@CollectionCodec.context, + parent = this@irGetOrCreateHelper.parent, + valueParameters = listOf(elementCodec.encodedType.knOrNull!!), + returnType = elementCodec.localIrType, + content = { lambda -> + +irReturn(with(elementCodec) { irDecode(codecContext, lambda.valueParameters[0]) }) + } + )) + // Constructor param: Transformed --> Source encoding lambda + putValueArgument(3, irLambda( + context = this@CollectionCodec.context, + parent = this@irGetOrCreateHelper.parent, + valueParameters = listOf(elementCodec.localIrType), + returnType = elementCodec.encodedType.knOrNull!!, + content = { lambda -> + +irReturn(with(elementCodec) { irEncode(codecContext, lambda.valueParameters[0]) }) + } + )) + + } + } + + // jobjectArray -> Collection + override fun IrStatementsBuilder<*>.irDecode(irContext: IrCodecContext, jni: IrValueDeclaration): IrExpression { + val codec = irTemporary(irGetOrCreateHelper(irContext), "helper") + val decode = runtimeHelperClass.functions.single { it.name.asString() == "decodeInto${collectionKind.name}" } + return irCall(decode).apply { + dispatchReceiver = irGet(codec) + extensionReceiver = irGet(irContext.environment) + putValueArgument(0, irGet(jni)) + } + } + + // Collection -> jobjectArray + override fun IrStatementsBuilder<*>.irEncode(irContext: IrCodecContext, local: IrValueDeclaration): IrExpression { + val codec = irTemporary(irGetOrCreateHelper(irContext), "helper") + val encode = runtimeHelperClass.functions.single { it.name.asString() == "encode${collectionKind.name}" } + return irCall(encode).apply { + dispatchReceiver = irGet(codec) + extensionReceiver = irGet(irContext.environment) + putValueArgument(0, irGet(local)) + } + } + + private fun String.toCollectionKind(arrayName: String, old: CollectionKind, new: CollectionKind): String { + return when (new) { + old -> this + CollectionKind.Set -> "${this}.toSet()" + CollectionKind.List -> "${this}.toList()" + CollectionKind.Array -> { + when (old) { + CollectionKind.Set -> "${this}.to${arrayName}Array()" + CollectionKind.List -> "${this}.to${arrayName}Array()" + else -> error("Can't happen.") + } + } + } + } + + override fun CodeBlock.Builder.codegenDecode(codegenContext: CodegenCodecContext, jni: String): String { + val arrayName = when (elementCodec.localCodegenType.name) { + INT -> "Int" + BYTE -> "Byte" + BOOLEAN -> "Boolean" + CHAR -> "Char" + SHORT -> "Short" + LONG -> "Long" + FLOAT -> "Float" + DOUBLE -> "Double" + else -> "Typed" + } + + // We always receive an array from JNI, but we might have to map the individual elements + // to a different type, in which case the type will have to change to list. + return when (elementCodec.needsCodegenConversion) { + false -> jni.toCollectionKind(arrayName, CollectionKind.Array, collectionKind) + else -> { + val elementMapper = CodeBlock.builder().also { block -> + val res = with(elementCodec) { block.codegenDecode(codegenContext, "it") } + block.addStatement(res) + } + "$jni.map { ${elementMapper.build()} }".toCollectionKind(arrayName, CollectionKind.List, collectionKind) + } + } + } + + override fun CodeBlock.Builder.codegenEncode(codegenContext: CodegenCodecContext, local: String): String { + val arrayName = when (val type = elementCodec.encodedType) { + is JniType.Primitive -> type.jvmSimpleName + is JniType.Object -> "Typed" + else -> error("Not possible") + } + + return when (elementCodec.needsCodegenConversion) { + false -> local.toCollectionKind(arrayName, collectionKind, CollectionKind.Array) + else -> { + // sequence.map returns a sequence. + val elementMapper = CodeBlock.builder().also { block -> + val res = with(elementCodec) { block.codegenEncode(codegenContext, "it") } + block.addStatement(res) + } + elementMapper.build() + "$local.map { ${elementMapper.build()} }".toCollectionKind(arrayName, CollectionKind.List, + CollectionKind.Array + ) + } + } + } +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/codec/CollectionKind.kt b/knee-compiler-plugin/src/main/kotlin/codec/CollectionKind.kt new file mode 100644 index 0000000..65dd88c --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/codec/CollectionKind.kt @@ -0,0 +1,71 @@ +package io.deepmedia.tools.knee.plugin.compiler.codec + +import com.squareup.kotlinpoet.* +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import io.deepmedia.tools.knee.plugin.compiler.codegen.CodegenType +import io.deepmedia.tools.knee.plugin.compiler.symbols.KneeSymbols +import io.deepmedia.tools.knee.plugin.compiler.utils.simple +import org.jetbrains.kotlin.ir.types.IrSimpleType +import org.jetbrains.kotlin.ir.types.IrType +import org.jetbrains.kotlin.ir.types.defaultType +import org.jetbrains.kotlin.ir.types.typeOrNull +import org.jetbrains.kotlin.ir.types.typeWith + +/** + * Utility to define different kinds of collections and also to collectionify the + * local types (both ir and codegen) for [CollectionCodec] implementation. + */ +enum class CollectionKind { + Array, List, Set; + + fun unwrapGeneric(itemType: IrType, symbols: KneeSymbols): IrType? { + // val generic = type.classOrNull?.owner?.defaultType ?: return null // .toBuilder().apply { arguments = emptyList() }.buildSimpleType() + val innerType = ((itemType as? IrSimpleType)?.arguments)?.singleOrNull()?.typeOrNull ?: return null + return when { + this == Array && itemType == symbols.builtIns.arrayClass.typeWith(innerType) -> innerType + this == Set && itemType == symbols.builtIns.setClass.typeWith(innerType) -> innerType + this == List && itemType == symbols.builtIns.listClass.typeWith(innerType) -> innerType + else -> null + } + } + + fun getCollectionTypeOf(itemType: IrType, symbols: KneeSymbols): IrSimpleType { + return when (this) { + Array -> when (itemType) { + symbols.builtIns.intType -> symbols.builtIns.intArray.defaultType + symbols.builtIns.booleanType -> symbols.builtIns.booleanArray.defaultType + symbols.builtIns.byteType -> symbols.builtIns.byteArray.defaultType + symbols.builtIns.charType -> symbols.builtIns.charArray.defaultType + symbols.builtIns.shortType -> symbols.builtIns.shortArray.defaultType + symbols.builtIns.longType -> symbols.builtIns.longArray.defaultType + symbols.builtIns.floatType -> symbols.builtIns.floatArray.defaultType + symbols.builtIns.doubleType -> symbols.builtIns.doubleArray.defaultType + else -> symbols.builtIns.arrayClass.typeWith(itemType) + } + Set -> symbols.builtIns.setClass.typeWith(itemType) + List -> symbols.builtIns.listClass.typeWith(itemType) + }.simple("CollectionKind.getCollectionTypeOf") + } + + fun getCollectionTypeOf(itemType: CodegenType, symbols: KneeSymbols): CodegenType { + if (itemType is CodegenType.IrBased) { + return CodegenType.from(getCollectionTypeOf(itemType.irType, symbols)) + } + val collectionType: TypeName = when (this) { + Array -> when (itemType.name) { + INT -> INT_ARRAY + BOOLEAN -> BOOLEAN_ARRAY + BYTE -> BYTE_ARRAY + CHAR -> CHAR_ARRAY + SHORT -> SHORT_ARRAY + LONG -> LONG_ARRAY + FLOAT -> FLOAT_ARRAY + DOUBLE -> DOUBLE_ARRAY + else -> ARRAY.parameterizedBy(itemType.name) + } + Set -> SET.parameterizedBy(itemType.name) + List -> LIST.parameterizedBy(itemType.name) + } + return CodegenType.from(collectionType) + } +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/codec/GenericCodec.kt b/knee-compiler-plugin/src/main/kotlin/codec/GenericCodec.kt new file mode 100644 index 0000000..d3808a7 --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/codec/GenericCodec.kt @@ -0,0 +1,106 @@ +package io.deepmedia.tools.knee.plugin.compiler.codec + +import com.squareup.kotlinpoet.ANY +import com.squareup.kotlinpoet.CodeBlock +import io.deepmedia.tools.knee.plugin.compiler.codegen.CodegenType +import io.deepmedia.tools.knee.plugin.compiler.symbols.KneeSymbols +import io.deepmedia.tools.knee.plugin.compiler.jni.JniType +import io.deepmedia.tools.knee.plugin.compiler.symbols.RuntimeIds.decodeBoxed +import io.deepmedia.tools.knee.plugin.compiler.symbols.RuntimeIds.encodeBoxed +import org.jetbrains.kotlin.ir.builders.* +import org.jetbrains.kotlin.ir.declarations.IrValueDeclaration +import org.jetbrains.kotlin.ir.expressions.IrExpression + +/** + * the "as any" codec - can wrap any other codec and is able to pass any value through JNI. + * It does so by using [JniType.Object], regardless of the wrapped codec [JniType], + * because it can transform between different jni types using a few tricks. + * + * It's very useful when type is not known as in generics - in many cases we want to + * know the function signature so we need a fixed [JniType]. This is what this does. + */ +class GenericCodec( + private val symbols: KneeSymbols, + innerCodec: Codec +) : Codec( + localIrType = innerCodec.localIrType, + localCodegenType = innerCodec.localCodegenType, + encodedType = JniType.Object(symbols, CodegenType.from(ANY.copy(nullable = true))) +) { + + private val wrappedCodec: Codec = innerCodec.wrappable() + private val wrappedType: JniType.Real = requireNotNull(wrappedCodec.encodedType as? JniType.Real) { + "Wrapped codec doesn't use a real JniType." + } + + /** Decoded value might be any JniType */ + override fun IrStatementsBuilder<*>.irEncode(irContext: IrCodecContext, local: IrValueDeclaration): IrExpression { + val data = when { + !wrappedCodec.needsIrConversion -> irGet(local) + else -> with(wrappedCodec) { irEncode(irContext, local) } + } + + fun irEncodeBoxed(type: String) = irCall(symbols.functions(encodeBoxed(type)).single()).apply { + putValueArgument(0, irGet(irContext.environment)) + putValueArgument(1, data) + } + + return when (wrappedType) { + is JniType.Object -> data // already a jobject + is JniType.Array -> data // already a jobject + is JniType.Long -> irEncodeBoxed("Long") + is JniType.Int -> irEncodeBoxed("Int") + is JniType.Double -> irEncodeBoxed("Double") + is JniType.Float -> irEncodeBoxed("Float") + is JniType.BooleanAsUByte -> irEncodeBoxed("Boolean") + is JniType.Byte -> irEncodeBoxed("Byte") + } + } + + override fun IrStatementsBuilder<*>.irDecode(irContext: IrCodecContext, jni: IrValueDeclaration): IrExpression { + + fun irDecodeBoxed(type: String) = irCall(symbols.functions(decodeBoxed(type)).single()).apply { + putValueArgument(0, irGet(irContext.environment)) + putValueArgument(1, irGet(jni)) + } + + val decoded = when (wrappedType) { + is JniType.Object -> irGet(jni) // irAs(irGet(jni), wrappedType.kn) + is JniType.Array -> irGet(jni) // irAs(irGet(jni), wrappedType.kn) + is JniType.Long -> irDecodeBoxed("Long") + is JniType.Int -> irDecodeBoxed("Int") + is JniType.Double -> irDecodeBoxed("Double") + is JniType.Float -> irDecodeBoxed("Float") + is JniType.BooleanAsUByte -> irDecodeBoxed("Boolean") + is JniType.Byte -> irDecodeBoxed("Byte") + } + return when { + !wrappedCodec.needsIrConversion -> decoded + else -> with(wrappedCodec) { irDecode(irContext, irTemporary(decoded)) } + } + } + + /** Encoded value comes as Any, here we should basically cast to T. */ + override fun CodeBlock.Builder.codegenDecode(codegenContext: CodegenCodecContext, jni: String): String { + return if (wrappedCodec.needsCodegenConversion) { + // TODO: "jni" might be an expression, not a variable. Can't use ${jni}_ safely. + addStatement("val ${jni}_ = $jni as %T", wrappedCodec.encodedType.jvmOrNull!!.name) + with(wrappedCodec) { codegenDecode(codegenContext, "${jni}_") } + } else { + "($jni) as ${wrappedCodec.encodedType.jvmOrNull!!.name}" + } + } + + /** Decoded value comes as T, which is already an instance of Any?. Nothing to do. */ + override fun CodeBlock.Builder.codegenEncode(codegenContext: CodegenCodecContext, local: String): String { + return if (wrappedCodec.needsCodegenConversion) { + with(wrappedCodec) { codegenEncode(codegenContext, local) } + } else { + local + } + } + + override fun toString(): String { + return "GenericCodec($wrappedCodec)" + } +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/codec/IdentityCodec.kt b/knee-compiler-plugin/src/main/kotlin/codec/IdentityCodec.kt new file mode 100644 index 0000000..e92cc30 --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/codec/IdentityCodec.kt @@ -0,0 +1,35 @@ +package io.deepmedia.tools.knee.plugin.compiler.codec + +import com.squareup.kotlinpoet.CodeBlock +import io.deepmedia.tools.knee.plugin.compiler.codec.Codec +import io.deepmedia.tools.knee.plugin.compiler.codec.CodegenCodecContext +import io.deepmedia.tools.knee.plugin.compiler.codec.IrCodecContext +import io.deepmedia.tools.knee.plugin.compiler.jni.JniType +import org.jetbrains.kotlin.ir.builders.IrStatementsBuilder +import org.jetbrains.kotlin.ir.builders.irGet +import org.jetbrains.kotlin.ir.declarations.IrValueDeclaration +import org.jetbrains.kotlin.ir.expressions.IrExpression + +/** + * A codec that needs no runtime transformations. This doesn't mean that kn and jvm types are identical, + * some transformations might be done by the JNI runtime itself, but there's else we should do at the ends + * of the bridge. + */ +class IdentityCodec(type: JniType.Real) : Codec(type.kn, type.jvm, type) { + override fun IrStatementsBuilder<*>.irDecode(irContext: IrCodecContext, jni: IrValueDeclaration): IrExpression { + return irGet(jni) + } + override fun IrStatementsBuilder<*>.irEncode(irContext: IrCodecContext, local: IrValueDeclaration): IrExpression { + return irGet(local) + } + override fun CodeBlock.Builder.codegenDecode(codegenContext: CodegenCodecContext, jni: String): String { + return jni + } + override fun CodeBlock.Builder.codegenEncode(codegenContext: CodegenCodecContext, local: String): String { + return local + } + + override fun toString(): String { + return "IdentityCodec(${encodedType::class.simpleName})" + } +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/codec/NullableCodec.kt b/knee-compiler-plugin/src/main/kotlin/codec/NullableCodec.kt new file mode 100644 index 0000000..3a88aa6 --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/codec/NullableCodec.kt @@ -0,0 +1,86 @@ +package io.deepmedia.tools.knee.plugin.compiler.codec + +import com.squareup.kotlinpoet.CodeBlock +import com.squareup.kotlinpoet.buildCodeBlock +import io.deepmedia.tools.knee.plugin.compiler.codegen.CodegenType +import io.deepmedia.tools.knee.plugin.compiler.symbols.KneeSymbols +import org.jetbrains.kotlin.ir.builders.* +import org.jetbrains.kotlin.ir.declarations.IrValueDeclaration +import org.jetbrains.kotlin.ir.expressions.IrExpression +import org.jetbrains.kotlin.ir.types.IrSimpleType +import org.jetbrains.kotlin.ir.types.withNullability + +/** + * IR ENCODING: if incoming value is irNull(), don't go through super class. Return it as is. + * IR DECODING: if incoming value is irNull(), don't go through super class. Return it as is. + * CODEGEN ENCODING: if incoming value is "null", don't go through super class. Return it as is. + * CODEGEN DECODING: if incoming value is "null", don't go through super class. Return it as is. + * + * Ideally we could just subclass GenericCodec but this codec definition must have the nulled types. + */ +class NullableCodec private constructor( + localIrType: IrSimpleType, + localCodegenType: CodegenType, + private val genericCodec: GenericCodec, + private val originalCodec: Codec +) : Codec( + localIrType = localIrType, + localCodegenType = localCodegenType, + encodedType = genericCodec.encodedType +) { + + constructor(symbols: KneeSymbols, notNullCodec: Codec) : this( + localIrType = notNullCodec.localIrType.withNullability(true), + localCodegenType = notNullCodec.localCodegenType.name.copy(nullable = true).let { CodegenType.from(it) }, + genericCodec = GenericCodec(symbols, notNullCodec), + originalCodec = notNullCodec + ) + + override fun IrStatementsBuilder<*>.irDecode(irContext: IrCodecContext, jni: IrValueDeclaration): IrExpression { + if (!genericCodec.needsIrConversion) return irGet(jni) + return irIfNull( + type = localIrType, + subject = irGet(jni), + thenPart = irNull(), + elsePart = irBlock { +with(genericCodec) { irDecode(irContext, jni) } } + ) + } + + override fun IrStatementsBuilder<*>.irEncode(irContext: IrCodecContext, local: IrValueDeclaration): IrExpression { + if (!genericCodec.needsIrConversion) return irGet(local) + return irIfNull( + type = encodedType.knOrNull!!, + subject = irGet(local), + thenPart = irNull(), + elsePart = irBlock { +with(genericCodec) { irEncode(irContext, local) } } + ) + } + + override fun CodeBlock.Builder.codegenDecode(codegenContext: CodegenCodecContext, jni: String): String { + if (!genericCodec.needsCodegenConversion) { + return jni + } + beginControlFlow("val ${jni}_: %T = if (($jni) == null) { $jni } else {", localCodegenType.name) + add(buildCodeBlock { + val processed = with(genericCodec) { codegenDecode(codegenContext, jni) } + addStatement(processed) + }) + endControlFlow() + return "${jni}_" + } + + override fun CodeBlock.Builder.codegenEncode(codegenContext: CodegenCodecContext, local: String): String { + if (!genericCodec.needsCodegenConversion) return local + beginControlFlow("val ${local}_: %T = if (($local) == null) { $local } else {", encodedType.jvmOrNull!!.name) + add(buildCodeBlock { + val processed = with(genericCodec) { codegenEncode(codegenContext, local) } + addStatement(processed) + }) + endControlFlow() + return "${local}_" + } + + override fun toString(): String { + return "$originalCodec?" + } +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/codec/PrimitiveCodecs.kt b/knee-compiler-plugin/src/main/kotlin/codec/PrimitiveCodecs.kt new file mode 100644 index 0000000..e6c9e8a --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/codec/PrimitiveCodecs.kt @@ -0,0 +1,48 @@ +package io.deepmedia.tools.knee.plugin.compiler.codec + +import com.squareup.kotlinpoet.CodeBlock +import io.deepmedia.tools.knee.plugin.compiler.context.KneeContext +import io.deepmedia.tools.knee.plugin.compiler.symbols.KneeSymbols +import io.deepmedia.tools.knee.plugin.compiler.jni.JniType +import io.deepmedia.tools.knee.plugin.compiler.symbols.RuntimeIds.encodeBoolean +import io.deepmedia.tools.knee.plugin.compiler.symbols.RuntimeIds.decodeBoolean +import org.jetbrains.kotlin.ir.builders.* +import org.jetbrains.kotlin.ir.declarations.IrValueDeclaration +import org.jetbrains.kotlin.ir.expressions.IrExpression +import org.jetbrains.kotlin.ir.types.IrSimpleType + +fun primitiveCodecs(context: KneeContext) = listOf( + // TODO: JVM shorts. Becomes jshort which is UChar in native. + // TODO: JVM chars. Becomes jchar which is UShort in native. + *IdentityCodec(JniType.Byte(context.symbols)).withCollectionCodecs(context), + *IdentityCodec(JniType.Int(context.symbols)).withCollectionCodecs(context), + *IdentityCodec(JniType.Long(context.symbols)).withCollectionCodecs(context), + *IdentityCodec(JniType.Float(context.symbols)).withCollectionCodecs(context), + *IdentityCodec(JniType.Double(context.symbols)).withCollectionCodecs(context), + // JVM booleans. Becomes jboolean which is UByte in native. + *BooleanCodec(context.symbols).withCollectionCodecs(context) +) + +private class BooleanCodec(symbols: KneeSymbols) : Codec(symbols.builtIns.booleanType as IrSimpleType, JniType.BooleanAsUByte(symbols)) { + private val create = symbols.functions(encodeBoolean).single() + private val decode = symbols.functions(decodeBoolean).single() + + override fun IrStatementsBuilder<*>.irDecode(irContext: IrCodecContext, jni: IrValueDeclaration): IrExpression { + return irCall(decode).apply { + putValueArgument(0, irGet(jni)) + } + } + + override fun IrStatementsBuilder<*>.irEncode(irContext: IrCodecContext, local: IrValueDeclaration): IrExpression { + return irCall(create).apply { + putValueArgument(0, irGet(local)) + } + } + + override fun CodeBlock.Builder.codegenDecode(codegenContext: CodegenCodecContext, jni: String) = jni + override fun CodeBlock.Builder.codegenEncode(codegenContext: CodegenCodecContext, local: String) = local + + override fun toString(): String { + return "BooleanCodec" + } +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/codec/StringCodecs.kt b/knee-compiler-plugin/src/main/kotlin/codec/StringCodecs.kt new file mode 100644 index 0000000..005bd86 --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/codec/StringCodecs.kt @@ -0,0 +1,50 @@ +package io.deepmedia.tools.knee.plugin.compiler.codec + +import com.squareup.kotlinpoet.CodeBlock +import com.squareup.kotlinpoet.STRING +import io.deepmedia.tools.knee.plugin.compiler.codegen.CodegenType +import io.deepmedia.tools.knee.plugin.compiler.context.KneeContext +import io.deepmedia.tools.knee.plugin.compiler.symbols.RuntimeIds.encodeString +import io.deepmedia.tools.knee.plugin.compiler.symbols.RuntimeIds.decodeString +import io.deepmedia.tools.knee.plugin.compiler.symbols.KneeSymbols +import io.deepmedia.tools.knee.plugin.compiler.jni.JniType +import org.jetbrains.kotlin.ir.builders.* +import org.jetbrains.kotlin.ir.declarations.IrValueDeclaration +import org.jetbrains.kotlin.ir.expressions.IrExpression +import org.jetbrains.kotlin.ir.types.IrSimpleType + +fun stringCodecs(context: KneeContext): List = listOf( + StringCodec(context.symbols) // .withCollectionCodecs(context) +) + +private class StringCodec(symbols: KneeSymbols) : Codec( + localType = symbols.builtIns.stringType as IrSimpleType, + encodedType = JniType.Object(symbols, CodegenType.from(STRING)) +) { + private val encode = symbols.functions(encodeString).single() + private val decode = symbols.functions(decodeString).single() + + override fun IrStatementsBuilder<*>.irDecode(irContext: IrCodecContext, jni: IrValueDeclaration): IrExpression { + return irCall(decode).apply { + putValueArgument(0, irGet(irContext.environment)) + putValueArgument(1, irGet(jni)) + } + } + + override fun IrStatementsBuilder<*>.irEncode(irContext: IrCodecContext, local: IrValueDeclaration): IrExpression { + return irCall(encode).apply { + putValueArgument(0, irGet(irContext.environment)) + putValueArgument(1, irGet(local)) + } + } + + // In codegen / JVM world, a String is already a String - nothing to do + + override fun CodeBlock.Builder.codegenDecode(codegenContext: CodegenCodecContext, jni: String): String { + return jni + } + + override fun CodeBlock.Builder.codegenEncode(codegenContext: CodegenCodecContext, local: String): String { + return local + } +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/codec/UnitCodecs.kt b/knee-compiler-plugin/src/main/kotlin/codec/UnitCodecs.kt new file mode 100644 index 0000000..f8d6e2b --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/codec/UnitCodecs.kt @@ -0,0 +1,86 @@ +package io.deepmedia.tools.knee.plugin.compiler.codec + +import com.squareup.kotlinpoet.CodeBlock +import io.deepmedia.tools.knee.plugin.compiler.context.KneeContext +import io.deepmedia.tools.knee.plugin.compiler.symbols.KneeSymbols +import io.deepmedia.tools.knee.plugin.compiler.jni.JniType +import io.deepmedia.tools.knee.plugin.compiler.utils.irError +import org.jetbrains.kotlin.ir.builders.* +import org.jetbrains.kotlin.ir.declarations.IrValueDeclaration +import org.jetbrains.kotlin.ir.expressions.IrExpression +import org.jetbrains.kotlin.ir.types.IrSimpleType + +fun unitCodecs(context: KneeContext) = listOf( + // void type, only for return types + ReturnVoidCodec(context.symbols), + NothingCodec(context.symbols) +) + + +/** + * As per [JniType.Void] definition, only return type is allowed. + * This uses [Codec.wrappable] to return a codec that really encodes the unit type. + */ +class ReturnVoidCodec(symbols: KneeSymbols) : Codec(symbols.builtIns.unitType as IrSimpleType, JniType.Void) { + + private val companion = UnitCodec(symbols) + + override val needsCodegenConversion: Boolean = true // get a chance to throw + override val needsIrConversion: Boolean = true // get a chance to throw + private fun ensure(condition: Boolean) { + check(condition) { "kotlin.Unit can't be used as a function parameter, only as return type." } + } + + override fun IrStatementsBuilder<*>.irDecode(irContext: IrCodecContext, jni: IrValueDeclaration): IrExpression { + ensure(irContext.decodesReturn) + return irGet(jni) + } + + override fun IrStatementsBuilder<*>.irEncode(irContext: IrCodecContext, local: IrValueDeclaration): IrExpression { + ensure(irContext.encodesReturn) + return irGet(local) + } + + override fun CodeBlock.Builder.codegenDecode(codegenContext: CodegenCodecContext, jni: String): String { + ensure(codegenContext.decodesReturn) + return jni + } + + override fun CodeBlock.Builder.codegenEncode(codegenContext: CodegenCodecContext, local: String): String { + ensure(codegenContext.encodesReturn) + return local + } + + override fun wrappable(): Codec = companion +} + +class UnitCodec(private val symbols: KneeSymbols) : Codec(symbols.builtIns.unitType as IrSimpleType, JniType.Int(symbols)) { + override fun IrStatementsBuilder<*>.irDecode(irContext: IrCodecContext, jni: IrValueDeclaration): IrExpression { + return irGetObject(symbols.builtIns.unitClass) + } + override fun IrStatementsBuilder<*>.irEncode(irContext: IrCodecContext, local: IrValueDeclaration): IrExpression { + return irInt(0) + } + override fun CodeBlock.Builder.codegenEncode(codegenContext: CodegenCodecContext, local: String): String { + return "0" + } + override fun CodeBlock.Builder.codegenDecode(codegenContext: CodegenCodecContext, jni: String): String { + return "kotlin.Unit" + } +} + +class NothingCodec(private val symbols: KneeSymbols) : Codec(symbols.builtIns.nothingType as IrSimpleType, JniType.Int(symbols)) { + override fun IrStatementsBuilder<*>.irEncode(irContext: IrCodecContext, local: IrValueDeclaration): IrExpression { + return irInt(0) + } + override fun CodeBlock.Builder.codegenEncode(codegenContext: CodegenCodecContext, local: String): String { + return "0" + } + override fun IrStatementsBuilder<*>.irDecode(irContext: IrCodecContext, jni: IrValueDeclaration): IrExpression { + return irError(symbols, "kotlin.Nothing can't be decoded.") + } + override fun CodeBlock.Builder.codegenDecode(codegenContext: CodegenCodecContext, jni: String): String { + return "error(\"kotlin.Nothing can't be decoded.\")" + } +} + diff --git a/knee-compiler-plugin/src/main/kotlin/codec/UnsignedCodecs.kt b/knee-compiler-plugin/src/main/kotlin/codec/UnsignedCodecs.kt new file mode 100644 index 0000000..1914b01 --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/codec/UnsignedCodecs.kt @@ -0,0 +1,67 @@ +package io.deepmedia.tools.knee.plugin.compiler.codec + +import com.squareup.kotlinpoet.CodeBlock +import io.deepmedia.tools.knee.plugin.compiler.symbols.KotlinIds +import io.deepmedia.tools.knee.plugin.compiler.symbols.KneeSymbols +import io.deepmedia.tools.knee.plugin.compiler.jni.JniType +import org.jetbrains.kotlin.backend.jvm.functionByName +import org.jetbrains.kotlin.ir.builders.IrStatementsBuilder +import org.jetbrains.kotlin.ir.builders.irCall +import org.jetbrains.kotlin.ir.builders.irGet +import org.jetbrains.kotlin.ir.declarations.IrValueDeclaration +import org.jetbrains.kotlin.ir.expressions.IrExpression +import org.jetbrains.kotlin.ir.types.typeWith +import org.jetbrains.kotlin.name.CallableId +import org.jetbrains.kotlin.name.ClassId + +private class UnsignedCodec( + symbols: KneeSymbols, + signed: JniType.Real, + unsignedClass: ClassId, + toUnsignedFunctions: CallableId, + toSignedFunction: String +): Codec( + localType = symbols.klass(unsignedClass).typeWith(), + encodedType = signed +) { + + private val description = "UnsignedCodec(${unsignedClass})" + + override fun toString(): String = description + + private val toUnsigned = symbols.functions(toUnsignedFunctions).single { + it.owner.extensionReceiverParameter?.type == signed.kn + } + private val toSigned = symbols.klass(unsignedClass).functionByName(toSignedFunction) + + override fun IrStatementsBuilder<*>.irDecode(irContext: IrCodecContext, jni: IrValueDeclaration): IrExpression { + return irCall(toUnsigned).apply { extensionReceiver = irGet(jni) } + } + + override fun IrStatementsBuilder<*>.irEncode(irContext: IrCodecContext, local: IrValueDeclaration): IrExpression { + return irCall(toSigned).apply { dispatchReceiver = irGet(local) } + } + + override fun CodeBlock.Builder.codegenEncode(codegenContext: CodegenCodecContext, local: String): String { + codegenContext.logger.injectLog(this, "$description ENCODING") + return "$local.${toSigned.owner.name.asString()}()" // uint.toInt() + } + + override fun CodeBlock.Builder.codegenDecode(codegenContext: CodegenCodecContext, jni: String): String { + codegenContext.logger.injectLog(this, "$description DECODING") + return "$jni.${toUnsigned.owner.name.asString()}()" // int.toUInt() + } +} + +private fun UInt(symbols: KneeSymbols) = UnsignedCodec(symbols, JniType.Int(symbols), KotlinIds.UInt, KotlinIds.toUInt, "toInt") +private fun ULong(symbols: KneeSymbols) = UnsignedCodec(symbols, JniType.Long(symbols), KotlinIds.ULong, KotlinIds.toULong, "toLong") +private fun UByte(symbols: KneeSymbols) = UnsignedCodec(symbols, JniType.Byte(symbols), KotlinIds.UByte, KotlinIds.toUByte, "toByte") + +fun unsignedCodecs(symbols: KneeSymbols) = listOf( + UInt(symbols), + ULong(symbols), + UByte(symbols), + // TODO: UShort + // TODO: UChar + // TODO: wrap in collections +) \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/codegen/CodegenDeclaration.kt b/knee-compiler-plugin/src/main/kotlin/codegen/CodegenDeclaration.kt new file mode 100644 index 0000000..cfa7397 --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/codegen/CodegenDeclaration.kt @@ -0,0 +1,153 @@ +package io.deepmedia.tools.knee.plugin.compiler.codegen + +import com.squareup.kotlinpoet.* +import io.deepmedia.tools.knee.plugin.compiler.utils.canonicalName +import io.deepmedia.tools.knee.plugin.compiler.utils.disambiguationName +import java.lang.IllegalStateException + +sealed class CodegenDeclaration constructor(val spec: T) { + + companion object { + private val RepeatableUids = listOf("constructor()") + } + + private val mutableChildren = mutableListOf>() + val children: List> get() = mutableChildren + + val descendants: Sequence> get() { + return sequence { + yield(this@CodegenDeclaration) + yieldAll(children.asSequence().flatMap { it.descendants }) + } + } + + abstract val uid: String + + abstract val packageName: String + + abstract val modifiers: List + + abstract override fun toString(): String + + inline fun > addChildIfNeeded(item: C): C { + val existing = children.firstOrNull { it.uid == item.uid } + return if (existing != null) { existing as C } else { item.also { addChild(it) } } + } + + fun addChild(item: CodegenDeclaration<*>) { + require(item.uid in RepeatableUids || children.none { it.uid == item.uid }) { + val others = children.filter { it.uid == item.uid } + "Already have item with id '${item.uid}'. Use addChildIfNeeded?\n\titem=$item\n\texisting=$others\n\tself=$this" + } + mutableChildren.add(item) + item.onAddedToParent(this) + } + + fun addChildren(vararg items: CodegenDeclaration<*>) { + items.forEach { addChild(it) } + } + + protected open fun onAddedToParent(parent: CodegenDeclaration<*>) {} +} + +class CodegenFile(spec: FileSpec.Builder) : CodegenDeclaration(spec) { + override val uid by lazy { "File(${spec.name})" } + override val modifiers = emptyList() + override fun toString() = spec.build().toString() + override val packageName: String = spec.packageName + val fileName: String = spec.name // without .kt extension +} + +class CodegenFunction(spec: FunSpec.Builder, val isPrimaryConstructor: Boolean = false) : CodegenDeclaration(spec) { + override val uid by lazy { + "Fun(${spec.build().name}, ${spec.parameters.joinToString { parameterSpec -> + parameterSpec.type.disambiguationName + }})" + } + + override val modifiers get() = spec.modifiers + + override fun toString() = spec.build().toString() + + val isGetter get() = spec.build().name == FunSpec.getterBuilder().build().name + val isSetter get() = spec.build().name == FunSpec.setterBuilder().build().name + + override lateinit var packageName: String + private set + + override fun onAddedToParent(parent: CodegenDeclaration<*>) { + super.onAddedToParent(parent) + packageName = parent.packageName + } +} + +class CodegenClass(spec: TypeSpec.Builder) : CodegenDeclaration(spec) { + private fun TypeSpec.Builder.tempBuild(): TypeSpec { + return try { build() } catch (e: Throwable) { + if (e.message?.contains("Functional interfaces must have exactly one abstract function. Contained 0: []") == true) { + val tempFun = FunSpec.builder("FAKEFUNCTION") + .addModifiers(KModifier.ABSTRACT) + .build() + addFunction(tempFun) + build().also { funSpecs.remove(tempFun) } + } else throw e + } + } + override val uid: String by lazy { + val build = spec.tempBuild() + var name = when { + build.name != null -> build.name!! + build.isCompanion -> "" + else -> error("Unexpected type (anon. class?): $build") + } + if (build.typeVariables.isNotEmpty()) { + name += build.typeVariables.joinToString(prefix = "<", postfix = ">") { + it.name + } + } + "Class(${name})" + } + + override val modifiers get() = spec.modifiers.toList() + + override fun toString() = spec.tempBuild().toString() + + val isCompanion get() = spec.tempBuild().isCompanion + val isInterface get() = spec.tempBuild().kind == TypeSpec.Kind.INTERFACE + val isObject get() = spec.tempBuild().kind == TypeSpec.Kind.OBJECT + + override lateinit var packageName: String + private set + + lateinit var type: CodegenType + private set + + override fun onAddedToParent(parent: CodegenDeclaration<*>) { + super.onAddedToParent(parent) + packageName = parent.packageName + type = when (parent) { + is CodegenFile -> CodegenType.from(packageName + "." + spec.tempBuild().name!!) + is CodegenClass -> when { + isCompanion -> CodegenType.from(parent.type.name.canonicalName + ".Companion") + else -> CodegenType.from(parent.type.name.canonicalName + "." + spec.tempBuild().name!!) + } + else -> error("CodegenClass added to invalid parent: $parent") + } + } +} + +class CodegenProperty(spec: PropertySpec.Builder) : CodegenDeclaration(spec) { + override val uid by lazy { "Property(${spec.build().name})" } + + override val modifiers get() = spec.modifiers + + override fun toString() = spec.build().toString() + + override lateinit var packageName: String + private set + + override fun onAddedToParent(parent: CodegenDeclaration<*>) { + super.onAddedToParent(parent) + packageName = parent.packageName + } +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/codegen/CodegenType.kt b/knee-compiler-plugin/src/main/kotlin/codegen/CodegenType.kt new file mode 100644 index 0000000..a7859e9 --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/codegen/CodegenType.kt @@ -0,0 +1,77 @@ +package io.deepmedia.tools.knee.plugin.compiler.codegen + +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.ParameterizedTypeName +import com.squareup.kotlinpoet.TypeName +import io.deepmedia.tools.knee.plugin.compiler.serialization.TypeNameSerializer +import io.deepmedia.tools.knee.plugin.compiler.utils.asTypeName +import io.deepmedia.tools.knee.plugin.compiler.utils.codegenClassId +import kotlinx.serialization.Contextual +import kotlinx.serialization.Serializable +import org.jetbrains.kotlin.ir.types.IrSimpleType +import org.jetbrains.kotlin.ir.types.IrType +import org.jetbrains.kotlin.ir.types.classOrNull +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.resolve.jvm.JvmClassName + +@Serializable +sealed class CodegenType { + + abstract val name: TypeName + + @Serializable + data class IrBased(@Contextual val irType: IrSimpleType) : CodegenType() { + override val name: TypeName by lazy { irType.asTypeName() } + } + + @Serializable + data class KpBased(@Serializable(with = TypeNameSerializer::class) override val name: TypeName) : CodegenType() + + override fun equals(other: Any?): Boolean { + if (other !is CodegenType) return false + return name == other.name + } + + override fun hashCode(): Int { + return name.hashCode() + } + + // my/class/name/OuterClass$InnerClass + // Need special logic for String and possibly other types that I'm missing at the moment + val jvmClassName: String get() { + val name = when (this) { + is IrBased -> { + val elementClassId = requireNotNull(irType.classOrNull?.owner?.codegenClassId) { + "Invalid CodegenType $irType (no fq name)" + } + JvmClassName.byClassId(elementClassId) + } + is KpBased -> { + val kpClass: ClassName = when (val type = name) { + is ClassName -> type + is ParameterizedTypeName -> type.rawType + else -> error("Unsupported kotlinpoet type: $type") + } + val dotsAndDollars = kpClass.reflectionName() // dots + dollar sign + JvmClassName.byFqNameWithoutInnerClasses(dotsAndDollars) + } + }.internalName + return when { + name == "kotlin/String" -> "java/lang/String" + name == "kotlin/Any" -> "java/lang/Object" + // Lambdas do not exist on the JVM. kotlin/FunctionX => kotlin/jvm/functions/FunctionX + name.startsWith("kotlin/Function") -> "kotlin/jvm/functions/Function${name.drop(15).toInt()}" + // Suspend lambdas do not exist either. kotlin/coroutines/SuspendFunctionX => kotlin/jvm/functions/Function(X+1) + // The extra parameter is for the continuation. + name.startsWith("kotlin/coroutines/SuspendFunction") -> "kotlin/jvm/functions/Function${name.drop(33).toInt() + 1}" + else -> name + } + } + + companion object { + fun from(irType: IrSimpleType): CodegenType = IrBased(irType) + fun from(fqName: String): CodegenType = KpBased(ClassName.bestGuess(fqName)) + fun from(fqName: FqName): CodegenType = KpBased(ClassName.bestGuess(fqName.asString())) + fun from(poetType: TypeName): CodegenType = KpBased(poetType) + } +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/codegen/KneeCodegen.kt b/knee-compiler-plugin/src/main/kotlin/codegen/KneeCodegen.kt new file mode 100644 index 0000000..6d87994 --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/codegen/KneeCodegen.kt @@ -0,0 +1,166 @@ +package io.deepmedia.tools.knee.plugin.compiler.codegen + +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.KModifier +import com.squareup.kotlinpoet.TypeSpec +import io.deepmedia.tools.knee.plugin.compiler.context.KneeContext +import io.deepmedia.tools.knee.plugin.compiler.import.ImportInfo +import io.deepmedia.tools.knee.plugin.compiler.import.writableParent +import io.deepmedia.tools.knee.plugin.compiler.utils.asPropertySpec +import io.deepmedia.tools.knee.plugin.compiler.utils.asTypeSpec +import io.deepmedia.tools.knee.plugin.compiler.utils.canonicalName +import org.jetbrains.kotlin.backend.jvm.ir.propertyIfAccessor +import org.jetbrains.kotlin.ir.declarations.* +import org.jetbrains.kotlin.ir.util.* +import org.jetbrains.kotlin.name.FqName +import java.io.File + +class KneeCodegen(private val context: KneeContext, val root: File, val verbose: Boolean) { + companion object { + const val Filename = "Knee" + } + init { + root.deleteRecursively() + root.mkdirs() + } + + private val files = mutableMapOf() + + private fun file(packageName: String) = files.getOrPut(packageName) { + CodegenFile(FileSpec.builder(packageName, Filename)) + } + + fun findExistingClass(name: FqName): CodegenClass? { + return files.values.asSequence() + .flatMap { it.descendants } + .filterIsInstance() + .firstOrNull { + it.type.name.canonicalName == name.asString() + } + } + + fun prepareContainer( + declaration: IrDeclaration, + importInfo: ImportInfo?, + detectPropertyAccessors: Boolean = true, + createCompanionObject: Boolean = false, + ): CodegenDeclaration<*> { + val irHierarchy: MutableList = when (val container = declaration.writableParent(context, importInfo)) { + is IrDeclaration -> container.parentsWithSelf.toMutableList() + else -> mutableListOf(container) + } + + // irHierarchy is a list which goes from the parent of declaration up until the file + // [parentOfDeclaration, ... , ... , declarationFile] + // We will then go from last to first and add all needed CodegenDeclarations + var candidate: CodegenDeclaration<*> = file((irHierarchy.removeLast() as IrFile).packageFqName.asString()) + + while (irHierarchy.isNotEmpty()) { + val irParent = irHierarchy.removeLast() + require(irParent is IrClass) { "Declaration parent is not an IrClass: $irParent (import=$importInfo all=${irHierarchy})" } + val codegenParent = irParent.asTypeSpec() + candidate = candidate.addChildIfNeeded(CodegenClass(codegenParent)) + } + + if (createCompanionObject && candidate is CodegenClass && !candidate.isCompanion) { + candidate = candidate.addChildIfNeeded(CodegenClass(TypeSpec.companionObjectBuilder())) + } + + if (detectPropertyAccessors && (declaration.isSetter || declaration.isGetter)) { + // The parent of a setter/getter is actually the property. + declaration as IrFunction + val irProperty = declaration.propertyIfAccessor as IrProperty + candidate = candidate.addChildIfNeeded(CodegenProperty(irProperty.asPropertySpec())) + } + return candidate + } + + /* fun containerOf( + declaration: IrDeclaration, + importInfo: ImportInfo?, + detectPropertyAccessors: Boolean = true, + createCompanionObject: Boolean = false, + replaceParentClassName: ((String) -> String)? = null, + ): CodegenDeclaration<*> { + val parents = declaration.parents.toList() + .reversed() + .dropWhile { it !is IrPackageFragment } + .toMutableList() + require(parents.removeFirstOrNull() is IrPackageFragment) { "First parent of $declaration is not IrPackageFragment, this is unexpected." } + + var candidate: CodegenDeclaration<*> = file(declaration.writableFile(context.module, importInfo).fqName.asString()) + + while (parents.isNotEmpty()) { + val irParent = parents.removeFirst() + require(irParent is IrClass) { "Declaration parent is not an IrClass: $irParent" } + val codegenParent = irParent.asTypeSpec(replaceParentClassName?.takeIf { parents.isEmpty() }) + candidate = candidate.maybePut(CodegenClass(codegenParent)) + } + + if (createCompanionObject && candidate is CodegenClass && !candidate.isCompanion) { + candidate = candidate.maybePut(CodegenClass(TypeSpec.companionObjectBuilder())) + } + + if (detectPropertyAccessors && (declaration.isSetter || declaration.isGetter)) { + // The parent of a setter/getter is actually the property. + declaration as IrFunction + val irProperty = declaration.propertyIfAccessor as IrProperty + candidate = candidate.maybePut(CodegenProperty(irProperty.asPropertySpec())) + } + return candidate + } */ + + fun write() { + files.values.forEach { spec -> + spec.prepare().build().writeTo(root) + } + } + + private fun CodegenDeclaration<*>.isProbablyPublic(): Boolean { + return !modifiers.contains(KModifier.PRIVATE) && !modifiers.contains(KModifier.INTERNAL) + } + + private fun CodegenDeclaration.prepare(): T { + val sorted = children.sortedByDescending { it.isProbablyPublic() } + sorted.forEach { + when (it) { + is CodegenFile -> error("CodegenFile can't be a children of anything else.") + is CodegenFunction -> { + val funSpec = it.prepare().build() + when (this) { + is CodegenFile -> spec.addFunction(funSpec) + is CodegenClass -> when { + it.isPrimaryConstructor -> spec.primaryConstructor(funSpec) + else -> spec.addFunction(funSpec) // works for regular constructors too + } + is CodegenProperty -> when { + it.isGetter -> spec.getter(funSpec) + it.isSetter -> spec.setter(funSpec) + else -> error("Can't add CodegenFunction to CodegenProperty, name = ${funSpec.name}") + } + is CodegenFunction -> error("Can't add CodegenFunction to CodegenFunction") + } + } + is CodegenClass -> { + val typeSpec = it.prepare().build() + when (this) { + is CodegenFile -> spec.addType(typeSpec) + is CodegenClass -> spec.addType(typeSpec) + is CodegenProperty -> error("Can't add CodegenType to CodegenProperty") + is CodegenFunction -> error("Can't add CodegenType to CodegenFunction") + } + } + is CodegenProperty -> { + val propertySpec = it.prepare().build() + when (this) { + is CodegenFile -> spec.addProperty(propertySpec) + is CodegenClass -> spec.addProperty(propertySpec) + is CodegenProperty -> error("Can't add CodegenProperty to CodegenProperty") + is CodegenFunction -> error("Can't add CodegenProperty to CodegenFunction") + } + } + } + } + return spec + } +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/context/KneeContext.kt b/knee-compiler-plugin/src/main/kotlin/context/KneeContext.kt new file mode 100644 index 0000000..b96b68f --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/context/KneeContext.kt @@ -0,0 +1,46 @@ +package io.deepmedia.tools.knee.plugin.compiler.context + +import io.deepmedia.tools.knee.plugin.compiler.serialization.IrClassListSerializer +import io.deepmedia.tools.knee.plugin.compiler.serialization.IrClassSerializer +import io.deepmedia.tools.knee.plugin.compiler.serialization.IrSimpleTypeSerializer +import io.deepmedia.tools.knee.plugin.compiler.symbols.KneeSymbols +import kotlinx.serialization.json.Json +import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.modules.contextual +import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext +import org.jetbrains.kotlin.cli.common.messages.MessageCollector +import org.jetbrains.kotlin.ir.declarations.* +import org.jetbrains.kotlin.ir.util.file + + +object KneeOrigin { + val KNEE by IrDeclarationOriginImpl.Synthetic + val KNEE_IMPORT_PARENT by IrDeclarationOriginImpl.Synthetic +} + +class KneeContext( + val plugin: IrPluginContext, + log: MessageCollector, + verboseLogs: Boolean, + verboseRuntime: Boolean, + val module: IrModuleFragment, + val useExport2: Boolean +) { + + val factory get() = plugin.irFactory + + val symbols = KneeSymbols(plugin) + + val json = Json { + serializersModule = SerializersModule { + contextual(IrClassSerializer(symbols)) + contextual(IrClassListSerializer(symbols)) + contextual(IrSimpleTypeSerializer(symbols)) + } + } + + val mapper by lazy { KneeMapper(this, json) } + + val log = KneeLogger(log, verboseLogs, verboseRuntime) +} + diff --git a/knee-compiler-plugin/src/main/kotlin/context/KneeLogger.kt b/knee-compiler-plugin/src/main/kotlin/context/KneeLogger.kt new file mode 100644 index 0000000..eb750f3 --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/context/KneeLogger.kt @@ -0,0 +1,52 @@ +package io.deepmedia.tools.knee.plugin.compiler.context + +import com.squareup.kotlinpoet.CodeBlock +import com.squareup.kotlinpoet.MemberName +import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity +import org.jetbrains.kotlin.cli.common.messages.MessageCollector +import org.jetbrains.kotlin.ir.builders.* +import org.jetbrains.kotlin.ir.declarations.IrDeclaration +import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol +import org.jetbrains.kotlin.ir.util.file +import org.jetbrains.kotlin.name.Name + +class KneeLogger( + private val collector: MessageCollector, + private val verboseLogs: Boolean, + private val verboseRuntime: Boolean +) { + + fun logWarning(message: String) { + if (verboseLogs) println(message) + collector.report(CompilerMessageSeverity.WARNING, message) + } + + fun logMessage(message: String) { + if (verboseLogs) println(message) + } + + private var printlnIr: IrSimpleFunctionSymbol? = null + private val printlnCodegen = MemberName("kotlin", "println") + + fun injectLog(scope: IrStatementsBuilder<*>, message: String) { + if (!verboseRuntime) return + + if (printlnIr == null) { + val builtIns = (scope.parent as IrDeclaration).file.module.irBuiltins + val function = builtIns.findFunctions(Name.identifier("println"), "kotlin", "export") + printlnIr = function.single { it.owner.valueParameters.firstOrNull()?.type == builtIns.stringType } + } + + with(scope) { + +irCall(printlnIr!!).apply { + putValueArgument(0, scope.irString("[KNEE_KN] $message")) + } + } + } + + + fun injectLog(scope: CodeBlock.Builder, message: String) { + if (!verboseRuntime) return + scope.addStatement("%M(%S)", printlnCodegen, "[KNEE_JVM] $message") + } +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/context/KneeMapper.kt b/knee-compiler-plugin/src/main/kotlin/context/KneeMapper.kt new file mode 100644 index 0000000..7c5026f --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/context/KneeMapper.kt @@ -0,0 +1,151 @@ +package io.deepmedia.tools.knee.plugin.compiler.context + +import io.deepmedia.tools.knee.plugin.compiler.codec.* +import io.deepmedia.tools.knee.plugin.compiler.codegen.CodegenType +import io.deepmedia.tools.knee.plugin.compiler.export.v1.ExportedCodec1 +import io.deepmedia.tools.knee.plugin.compiler.export.v1.exportInfo +import io.deepmedia.tools.knee.plugin.compiler.export.v2.ExportedCodec2 +import io.deepmedia.tools.knee.plugin.compiler.export.v2.ExportedTypeInfo +import io.deepmedia.tools.knee.plugin.compiler.jni.JniType +import io.deepmedia.tools.knee.plugin.compiler.metadata.ModuleMetadata +import io.deepmedia.tools.knee.plugin.compiler.symbols.AnnotationIds +import io.deepmedia.tools.knee.plugin.compiler.utils.asTypeName +import io.deepmedia.tools.knee.plugin.compiler.utils.isPartOf +import io.deepmedia.tools.knee.plugin.compiler.utils.simple +import kotlinx.serialization.json.Json +import org.jetbrains.kotlin.ir.backend.js.utils.valueArguments +import org.jetbrains.kotlin.ir.declarations.IrAnnotationContainer +import org.jetbrains.kotlin.ir.declarations.IrClass +import org.jetbrains.kotlin.ir.expressions.IrConst +import org.jetbrains.kotlin.ir.expressions.IrConstructorCall +import org.jetbrains.kotlin.ir.expressions.IrExpression +import org.jetbrains.kotlin.ir.types.* +import org.jetbrains.kotlin.ir.util.* + +class KneeMapper( + private val context: KneeContext, + private val json: Json +) { + private val symbols = context.symbols + + private val builtInCodecs = listOf( + *unitCodecs(context).toTypedArray(), + *primitiveCodecs(context).toTypedArray(), + *unsignedCodecs(context.symbols).toTypedArray(), + *stringCodecs(context).toTypedArray(), + *bufferCodecs(context.symbols).toTypedArray(), + ) + + private val userDefinedCodecs = mutableListOf() + private val lazyCodecs = mutableListOf() + + var dependencies: Map = emptyMap() + + fun register(vararg codecs: Codec) { + this.userDefinedCodecs.addAll(codecs) + } + + // This representation is the best one because it shows type parameters + private val IrType.description get() = runCatching { this.simple("IrType.description").asTypeName() }.getOrElse { this }.toString() + + private val IrConstructorCall.description get() = "${type.description} ${valueArguments.map { it?.description }}" + + private val IrExpression.description get() = if (this is IrConst<*>) this.value.toString() else this.toString() + + private fun errorDescription(type: IrType): String { + val klass = type.classOrNull?.owner + if (klass != null + && !klass.isPartOf(context.module) + && listOf(AnnotationIds.KneeEnum, AnnotationIds.KneeClass, AnnotationIds.KneeInterface).any { klass.hasAnnotation(it) }) { + return """ + Type ${type.description} cannot be passed through the JNI bridge in module ${context.module.name}. + It is a Knee type defined in an external module${klass.fileOrNull?.let { " " + it.module.name } ?: ""}. + To make it serializable here, use export<${type.description}>() in the original KneeModule configuration block. + """.trimIndent() + } + + return """ +Type ${type.description} can not be passed through the JNI bridge. +Available: +${userDefinedCodecs.joinToString("\n") { "\t" + it.localIrType.description }} +Annotations: +${type.classOrNull?.owner?.annotations?.joinToString("\n") { "\t" + it.description }} +""".trimIndent() + } + + @Suppress("UNCHECKED_CAST") + fun get(type: IrType, useSiteAnnotations: IrAnnotationContainer? = null): Codec { + val raw = useSiteAnnotations?.getAnnotation(AnnotationIds.KneeRaw) + if (raw != null) { + val fqn = (raw.getValueArgument(0)!! as IrConst).value + val jobject = JniType.Object(context.symbols, CodegenType.from(fqn)) + require(jobject.kn.makeNullable() == type.makeNullable()) { + "@KneeRaw(${fqn}) should be applied on a parameter of type 'jobject' or similar CPointer type alias." + } + return IdentityCodec(type = jobject) + } + return getConcrete(type) + } + + private fun getConcrete(type: IrType): Codec { + return requireNotNull(getConcreteOrNull(type)) { errorDescription(type) } + } + + private fun getConcreteOrNull(type: IrType): Codec? { + val candidate + = userDefinedCodecs.firstOrNull { it.localIrType == type } + ?: builtInCodecs.firstOrNull { it.localIrType == type } + ?: lazyCodecs.firstOrNull { it.localIrType == type } + if (candidate != null) return candidate + + if (type.isNullable()) { + val notNull = getConcreteOrNull(type.makeNotNull()) + if (notNull != null) return NullableCodec(symbols, notNull) + } + + val inner = CollectionKind.entries.firstNotNullOfOrNull { it.unwrapGeneric(type, symbols) } + if (inner != null) { + val wrappers = getConcreteOrNull(inner)?.collectionCodecs(context) + if (wrappers != null) { + return wrappers.first { it.localIrType == type }.also { + lazyCodecs.addAll(wrappers) + } + } + } + + // export1 + val typeClass = type.classOrNull?.owner + if (typeClass != null && !typeClass.isPartOf(context.module)) { + val export1Info = typeClass.exportInfo + if (export1Info != null) { + return ExportedCodec1(symbols, type, export1Info).also { + lazyCodecs.add(it) + } + } + } + + // export2 + // that is: see if any one of our dependency modules declared the capability to export this + if (type is IrSimpleType) { + val exportInfo = dependencies.findModuleExportingType(type) + if (exportInfo != null) { + return ExportedCodec2(symbols, exportInfo.first, exportInfo.second).also { + lazyCodecs.add(it) + } + } + } + + return null + } + + private fun Map.findModuleExportingType(type: IrSimpleType): Pair? { + return firstNotNullOfOrNull { (klass, metadata) -> + if (metadata == null) return@firstNotNullOfOrNull null + val exportedType = metadata.exportedTypes.firstOrNull { it.localIrType == type } + if (exportedType != null) return klass to exportedType + val dependencies = metadata.dependencyModules.associateWith { ModuleMetadata.read(it, json)!! } + dependencies.findModuleExportingType(type) + } + } + +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/export/v1/ExportAdapters.kt b/knee-compiler-plugin/src/main/kotlin/export/v1/ExportAdapters.kt new file mode 100644 index 0000000..fd78b1e --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/export/v1/ExportAdapters.kt @@ -0,0 +1,133 @@ +package io.deepmedia.tools.knee.plugin.compiler.export.v1 + +import com.squareup.kotlinpoet.CodeBlock +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.KModifier +import com.squareup.kotlinpoet.TypeSpec +import io.deepmedia.tools.knee.plugin.compiler.ClassCodec +import io.deepmedia.tools.knee.plugin.compiler.EnumCodec +import io.deepmedia.tools.knee.plugin.compiler.InterfaceCodec +import io.deepmedia.tools.knee.plugin.compiler.codec.Codec +import io.deepmedia.tools.knee.plugin.compiler.codec.CodegenCodecContext +import io.deepmedia.tools.knee.plugin.compiler.codec.IrCodecContext +import io.deepmedia.tools.knee.plugin.compiler.codegen.CodegenClass +import io.deepmedia.tools.knee.plugin.compiler.codegen.KneeCodegen +import io.deepmedia.tools.knee.plugin.compiler.context.KneeContext +import io.deepmedia.tools.knee.plugin.compiler.import.ImportInfo +import io.deepmedia.tools.knee.plugin.compiler.import.concrete +import io.deepmedia.tools.knee.plugin.compiler.symbols.AnnotationIds +import io.deepmedia.tools.knee.plugin.compiler.utils.codegenFqName +import org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder +import org.jetbrains.kotlin.ir.builders.irBlockBody +import org.jetbrains.kotlin.ir.builders.irReturn +import org.jetbrains.kotlin.ir.builders.irReturnUnit +import org.jetbrains.kotlin.ir.declarations.IrClass +import org.jetbrains.kotlin.ir.util.defaultType +import org.jetbrains.kotlin.ir.util.functions +import org.jetbrains.kotlin.ir.util.hasAnnotation + +/** + * "Adapter": read and write functions on both the native and the JVM side. + * These can be later used at runtime by [ExportedCodec1]. + */ +object ExportAdapters { + + fun exportIfNeeded( + klass: IrClass, + context: KneeContext, + codegen: KneeCodegen, + importInfo: ImportInfo? + ) { + val exportInfo = klass.exportInfo ?: return + export(klass, exportInfo, context, codegen, importInfo) + } + + fun export( + klass: IrClass, + exportInfo: ExportInfo, + context: KneeContext, + codegen: KneeCodegen, + importInfo: ImportInfo? + ) { + val codec = context.mapper.get(klass.defaultType.concrete(importInfo)) + exportIr(klass, exportInfo.adapterNativeCoordinates, context, codec) + exportCodegen(klass, exportInfo.adapterJvmCoordinates, context, codec, codegen) + } + + private fun exportIr(klass: IrClass, location: ExportInfo.NativeCoordinates, context: KneeContext, codec: Codec) { + val export = klass.functions.first { it.name == ExportInfo.DeclarationNames.AnnotatedFunction } + export.body = DeclarationIrBuilder(context.plugin, export.symbol).irBlockBody { +irReturnUnit() } + + val spec: IrClass = when (location) { + is ExportInfo.NativeCoordinates.InnerObject -> klass.declarations + .filterIsInstance() + .first { it.name == location.name } + } + + val read = spec.functions.first { it.name.asString() == "read" } + val write = spec.functions.first { it.name.asString() == "write" } + + read.body = DeclarationIrBuilder(context.plugin, read.symbol).irBlockBody { + // Note: reverse = false but we don't relly know if the obj being converted is a param or return type + // TODO: reconsider this reverse flag as it does not generalize properly to export specs + val codecContext = + IrCodecContext(null, read.valueParameters[0], false, context.log) + with(codec) { + +irReturn(irDecode(codecContext, read.valueParameters[1])) + } + } + + write.body = DeclarationIrBuilder(context.plugin, write.symbol).irBlockBody { + // Note: reverse = false but we don't relly know if the obj being converted is a param or return type + // We should reconsider this reverse flag as it does not generalize properly to export specs + val codecContext = + IrCodecContext(null, write.valueParameters[0], false, context.log) + with(codec) { + +irReturn(irEncode(codecContext, write.valueParameters[1])) + } + } + } + + private fun exportCodegen(klass: IrClass, location: ExportInfo.JvmCoordinates, context: KneeContext, codec: Codec, codegen: KneeCodegen) { + val (codegenContainer, codegenName) = when (location) { + is ExportInfo.JvmCoordinates.InnerObject -> codegen.findExistingClass(name = klass.codegenFqName) to location.name + is ExportInfo.JvmCoordinates.ExternalObject -> codegen.findExistingClass(name = location.parent) to location.name + } + checkNotNull(codegenContainer) { + "Could not find codegen container for location: $location classFqName=${klass.codegenFqName}" + } + // Note: reverse = false but we don't relly know if the obj being converted is a param or return type + // We should reconsider this reverse flag as it does not generalize properly to export specs + val codecContext = CodegenCodecContext(null, false, context.log) + + codegenContainer.addChild(CodegenClass(TypeSpec.objectBuilder(codegenName.asString()).apply { + addModifiers(KModifier.PUBLIC) + val thisType = codegen.findExistingClass(name = klass.codegenFqName)!!.type.name // klass.defaultType.concrete(importInfo).asTypeName() + val jniType = when { + klass.hasAnnotation(AnnotationIds.KneeClass) -> ClassCodec.encodedTypeForIr(context.symbols) + klass.hasAnnotation(AnnotationIds.KneeEnum) -> EnumCodec.encodedTypeForIr(context.symbols) + klass.hasAnnotation(AnnotationIds.KneeInterface) -> InterfaceCodec.encodedTypeForIr(context.symbols) + else -> error("Exported class $klass is not enum nor interface nor class.") + }.jvmOrNull!!.name + funSpecs.add( + FunSpec.builder("read") + .addParameter("data", jniType, emptyList()) + .returns(thisType) + .addCode( + CodeBlock.builder() + .apply { addStatement("return ${with(codec) { codegenDecode(codecContext, "data") }}") } + .build()) + .build()) + funSpecs.add( + FunSpec.builder("write") + .addParameter("data", thisType, emptyList()) + .returns(jniType) + .addCode( + CodeBlock.builder() + .apply { addStatement("return ${with(codec) { codegenEncode(codecContext, "data") }}") } + .build()) + .build()) + })) + } + +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/export/v1/ExportFirDescriptors.kt b/knee-compiler-plugin/src/main/kotlin/export/v1/ExportFirDescriptors.kt new file mode 100644 index 0000000..ad193f5 --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/export/v1/ExportFirDescriptors.kt @@ -0,0 +1,172 @@ +package io.deepmedia.tools.knee.plugin.compiler.export.v1 + +import io.deepmedia.tools.knee.plugin.compiler.ClassCodec +import io.deepmedia.tools.knee.plugin.compiler.EnumCodec +import io.deepmedia.tools.knee.plugin.compiler.InterfaceCodec +import io.deepmedia.tools.knee.plugin.compiler.symbols.AnnotationIds +import io.deepmedia.tools.knee.plugin.compiler.symbols.CInteropIds +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import org.jetbrains.kotlin.descriptors.CallableMemberDescriptor +import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.descriptors.ClassKind +import org.jetbrains.kotlin.descriptors.DescriptorVisibilities +import org.jetbrains.kotlin.descriptors.Modality +import org.jetbrains.kotlin.descriptors.SimpleFunctionDescriptor +import org.jetbrains.kotlin.descriptors.SourceElement +import org.jetbrains.kotlin.descriptors.ValueParameterDescriptor +import org.jetbrains.kotlin.descriptors.annotations.AnnotationDescriptorImpl +import org.jetbrains.kotlin.descriptors.annotations.Annotations +import org.jetbrains.kotlin.descriptors.findClassAcrossModuleDependencies +import org.jetbrains.kotlin.descriptors.findTypeAliasAcrossModuleDependencies +import org.jetbrains.kotlin.descriptors.impl.SimpleFunctionDescriptorImpl +import org.jetbrains.kotlin.descriptors.impl.ValueParameterDescriptorImpl +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.psi.synthetics.SyntheticClassOrObjectDescriptor +import org.jetbrains.kotlin.resolve.constants.StringValue +import org.jetbrains.kotlin.resolve.descriptorUtil.builtIns +import org.jetbrains.kotlin.resolve.descriptorUtil.module +import org.jetbrains.kotlin.resolve.lazy.LazyClassContext +import org.jetbrains.kotlin.resolve.lazy.declarations.ClassMemberDeclarationProvider +import org.jetbrains.kotlin.types.KotlinType + +/** + * Given a certain [ownerDescriptor] class to be exported, this is able to + * create frontend (K1) declarations for: + * 1. the native adapter class ([adapterDescriptor], [makeAdapterDescriptor]) + * 2. its inner read and write functions ([adapterFunctionNames], [makeAdapterFunctionDescriptor]) + * 3. the dummy, carrier, annotated function ([annotatedFunctionName], [makeAnnotatedFunctionDescriptor]) + * This function is annotated with serialized version of [ExportInfo], so that consumers can read that. + * + * Note that 3. is fundamental for consumers to understand where the adapters are (on both native and JVM side). + * This is done in frontend because IR is not allowed to add annotations. + * + * The first two steps (native adapter with read/write functions) are also needed for consumer modules + * to be able to get the adapter class and its functions; if they wouldn't be written in FIR, they would + * not appear at all to consumers after klib deserialization. + * + * (We may skip read/write here if we make the adapter extend some known, compiled interface) + */ +class ExportFirDescriptors( + val ownerDescriptor: ClassDescriptor, +) { + + val exportInfo = ExportInfo( + adapterNativeCoordinates = ExportInfo.NativeCoordinates.compute(ownerDescriptor), + adapterJvmCoordinates = ExportInfo.JvmCoordinates.compute(ownerDescriptor), + ) + + val annotatedFunctionName: Name = ExportInfo.DeclarationNames.AnnotatedFunction + + fun makeAnnotatedFunctionDescriptor(): SimpleFunctionDescriptor { + val serializedExportInfo = Json.encodeToString(exportInfo) + + val annotatedFunctionDescriptor = SimpleFunctionDescriptorImpl.create( + ownerDescriptor, + Annotations.create(listOf( + AnnotationDescriptorImpl( + ownerDescriptor.module.findClassAcrossModuleDependencies(AnnotationIds.KneeMetadata)!!.defaultType, + mapOf(Name.identifier("metadata") to StringValue(serializedExportInfo)), + SourceElement.NO_SOURCE, // ownerDescriptor.source, // SourceElement.NO_SOURCE + ), + )), + annotatedFunctionName, + CallableMemberDescriptor.Kind.SYNTHESIZED, + ownerDescriptor.source, // SourceElement.NO_SOURCE + ) + annotatedFunctionDescriptor.initialize( + null, + ownerDescriptor.thisAsReceiverParameter, emptyList(), emptyList(), emptyList(), + ownerDescriptor.builtIns.unitType, Modality.FINAL, DescriptorVisibilities.PUBLIC + ) + return annotatedFunctionDescriptor + } + + // For now always place the adapter as inner object, but there might be cases where this is not desirable + // or not even possible (for example, imported stuff!). Need to check all edge cases + val adapterFunctionNames = listOf(Name.identifier("read"), Name.identifier("write")) + + fun makeAdapterFunctionDescriptor(parent: ClassDescriptor, name: Name): SimpleFunctionDescriptor { + val descriptor = SimpleFunctionDescriptorImpl.create(parent, Annotations.EMPTY, name, CallableMemberDescriptor.Kind.SYNTHESIZED, parent.source) + // NOTE: this type is unsubstituted! If generics come into play, it should go through substitution + val actualType = ownerDescriptor.defaultType + val jniType = when { + ownerDescriptor.annotations.hasAnnotation(AnnotationIds.KneeClass) -> ClassCodec.encodedTypeForFir(ownerDescriptor.module) + ownerDescriptor.annotations.hasAnnotation(AnnotationIds.KneeEnum) -> EnumCodec.encodedTypeForFir(ownerDescriptor.module) + ownerDescriptor.annotations.hasAnnotation(AnnotationIds.KneeInterface) -> InterfaceCodec.encodedTypeForFir(ownerDescriptor.module) + else -> error("Exported owner $ownerDescriptor is not enum nor interface nor class.") + } + val isRead = name.asString() == "read" + val returnType = when { + isRead -> actualType + else -> jniType + } + val inputType = when { + isRead -> jniType + else -> actualType + } + // Could use the JniEnvironment type alias, but whatever, it's still a pointer in the end. + val envType = ownerDescriptor.module + .findTypeAliasAcrossModuleDependencies(CInteropIds.COpaquePointer)!! + .expandedType + + fun KotlinType.asValueParameter(name: String, index: Int): ValueParameterDescriptor { + return ValueParameterDescriptorImpl( + containingDeclaration = descriptor, + original = null, + index = index, + annotations = Annotations.EMPTY, + name = Name.identifier(name), + outType = this, + declaresDefaultValue = false, + isCrossinline = false, + isNoinline = false, + varargElementType = null, + source = parent.source, + ) + } + descriptor.initialize(null, + parent.thisAsReceiverParameter, emptyList(), emptyList(), + listOf( + envType.asValueParameter("env", 0), + inputType.asValueParameter("data", 1), + ), + returnType, + Modality.FINAL, DescriptorVisibilities.PUBLIC + ) + return descriptor + } + + var adapterDescriptor: ClassDescriptor? = null + private set + + fun makeAdapterDescriptor(ctx: LazyClassContext, dp: ClassMemberDeclarationProvider, name: Name): ClassDescriptor { + val parent = requireNotNull(dp.correspondingClassOrObject) { "correspondingClassOrObject was null." } + + /* val scope = dp.ownerInfo?.let { + ctx.declarationScopeProvider.getResolutionScopeForDeclaration(it.scopeAnchor) + } ?: (ownerDescriptor as ClassDescriptorWithResolutionScopes).scopeForClassHeaderResolution */ + val scope = ctx.declarationScopeProvider.getResolutionScopeForDeclaration(dp.ownerInfo!!.scopeAnchor) + + // At first tried with ClassDescriptorImpl but it does not work, in that it does not trigger + // the next round of getSyntheticFunctionNames for example. + val objectDescriptor = SyntheticClassOrObjectDescriptor( + c = ctx, + parentClassOrObject = parent, + containingDeclaration = ownerDescriptor, + name = name, + source = ownerDescriptor.source, + outerScope = scope, + modality = Modality.FINAL, + visibility = DescriptorVisibilities.PUBLIC, + annotations = Annotations.EMPTY, + constructorVisibility = DescriptorVisibilities.PRIVATE, + kind = ClassKind.OBJECT, + isCompanionObject = false + ) + objectDescriptor.initialize() + return objectDescriptor.also { + adapterDescriptor = it + } + } +} diff --git a/knee-compiler-plugin/src/main/kotlin/export/v1/ExportFlags.kt b/knee-compiler-plugin/src/main/kotlin/export/v1/ExportFlags.kt new file mode 100644 index 0000000..35ed75c --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/export/v1/ExportFlags.kt @@ -0,0 +1,31 @@ +package io.deepmedia.tools.knee.plugin.compiler.export.v1 + +import io.deepmedia.tools.knee.plugin.compiler.symbols.AnnotationIds +import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.ir.declarations.IrClass +import org.jetbrains.kotlin.ir.expressions.IrConst +import org.jetbrains.kotlin.ir.util.getAnnotation +import org.jetbrains.kotlin.ir.util.getValueArgument +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.resolve.constants.BooleanValue + +val IrClass.hasExport1Flag: Boolean get() { + val e = getAnnotation(AnnotationIds.KneeClass) + ?: getAnnotation(AnnotationIds.KneeEnum) + ?: getAnnotation(AnnotationIds.KneeInterface) + ?: return false + + val a = e.getValueArgument(Name.identifier("exported")) ?: return false + @Suppress("UNCHECKED_CAST") + return (a as? IrConst)?.value ?: false +} + +val ClassDescriptor.hasExport1Flag: Boolean get() { + val e = annotations.findAnnotation(AnnotationIds.KneeClass) + ?: annotations.findAnnotation(AnnotationIds.KneeEnum) + ?: annotations.findAnnotation(AnnotationIds.KneeInterface) + ?: return false + + val arg = e.allValueArguments[Name.identifier("exported")] ?: return false + return (arg as? BooleanValue)?.value ?: false +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/export/v1/ExportInfo.kt b/knee-compiler-plugin/src/main/kotlin/export/v1/ExportInfo.kt new file mode 100644 index 0000000..ffd178e --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/export/v1/ExportInfo.kt @@ -0,0 +1,102 @@ +package io.deepmedia.tools.knee.plugin.compiler.export.v1 + +import io.deepmedia.tools.knee.plugin.compiler.instances.InterfaceNames.asInterfaceName +import io.deepmedia.tools.knee.plugin.compiler.serialization.FqNameSerializer +import io.deepmedia.tools.knee.plugin.compiler.serialization.NameSerializer +import io.deepmedia.tools.knee.plugin.compiler.symbols.AnnotationIds +import io.deepmedia.tools.knee.plugin.compiler.utils.* +import kotlinx.serialization.Serializable +import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.name.Name +import kotlinx.serialization.json.Json +import org.jetbrains.kotlin.ir.backend.js.utils.valueArguments +import org.jetbrains.kotlin.ir.declarations.IrClass +import org.jetbrains.kotlin.ir.expressions.IrConst +import org.jetbrains.kotlin.ir.util.functions + +/** + * For owned declarations, here we are reading in IR the information that was written in the frontend + * descriptor-based step. + * For external declarations, we are reading information provided by their own compiler invocation. + */ +@Suppress("UNCHECKED_CAST") +val IrClass.exportInfo: ExportInfo? get() { + if (!hasExport1Flag) return null + // Reading in backend IR the information we wrote in frontend descriptor step... + return functions + .first { it.name == ExportInfo.DeclarationNames.AnnotatedFunction } + .annotations + .single() + .valueArguments[0] + .let { it as IrConst } + .value + .let { Json.decodeFromString(it) } +} + +@Serializable +data class ExportInfo( + val adapterNativeCoordinates: NativeCoordinates, + val adapterJvmCoordinates: JvmCoordinates +) { + + object DeclarationNames { + /** A dummy function, added to the class to be exported, that will carry the annotation with serialized [ExportInfo] */ + val AnnotatedFunction = Name.identifier("Knee\$ExportInfoHandle") + /** + * Name of the read/write adapter, used for both native and JVM side (with some exceptions!) + * It doesn't matter though because it's already exposed in the location objects and that's where it should be read. + */ + val SyntheticAdapter = Name.identifier("Knee\$ExportAdapter") + } + + /** + * Location of the native side of the adapter. + */ + @Serializable + sealed class NativeCoordinates { + @Serializable + data class InnerObject(@Serializable(with = NameSerializer::class) val name: Name) : NativeCoordinates() + + companion object { + /** + * For now, all IR specs are written as an inner object named [DeclarationNames.SyntheticAdapter]. + * Soon we might need other options because this is not always desireable or even possible. + */ + fun compute(descriptor: ClassDescriptor): NativeCoordinates { + return InnerObject(DeclarationNames.SyntheticAdapter) + } + } + } + + @Serializable + sealed class JvmCoordinates { + @Serializable + data class InnerObject(@Serializable(with = NameSerializer::class) val name: Name) : JvmCoordinates() + + @Serializable + data class ExternalObject( + @Serializable(with = FqNameSerializer::class) val parent: FqName, + @Serializable(with = NameSerializer::class) val name: Name + ) : JvmCoordinates() + + companion object { + fun compute(descriptor: ClassDescriptor): JvmCoordinates { + return when { + descriptor.annotations.hasAnnotation(AnnotationIds.KneeClass) -> InnerObject(DeclarationNames.SyntheticAdapter) + descriptor.annotations.hasAnnotation(AnnotationIds.KneeEnum) -> InnerObject(DeclarationNames.SyntheticAdapter) + descriptor.annotations.hasAnnotation(AnnotationIds.KneeInterface) -> { + // Inner object of the impl class. + // We can use a nicer name since the spec is not a member of the user-facing class + val parentsName = descriptor.codegenFqName.parent() + val implName = descriptor.codegenName.asInterfaceName(null) + val mergedName = FqName("$parentsName.$implName") + ExternalObject(mergedName, Name.identifier("ExportSpec")) + } + else -> error("Exported owner $descriptor is not enum nor interface nor class.") + } + } + } + } +} + diff --git a/knee-compiler-plugin/src/main/kotlin/export/v1/ExportedCodec1.kt b/knee-compiler-plugin/src/main/kotlin/export/v1/ExportedCodec1.kt new file mode 100644 index 0000000..352efc8 --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/export/v1/ExportedCodec1.kt @@ -0,0 +1,101 @@ +package io.deepmedia.tools.knee.plugin.compiler.export.v1 + +import com.squareup.kotlinpoet.CodeBlock +import com.squareup.kotlinpoet.TypeName +import io.deepmedia.tools.knee.plugin.compiler.ClassCodec +import io.deepmedia.tools.knee.plugin.compiler.EnumCodec +import io.deepmedia.tools.knee.plugin.compiler.InterfaceCodec +import io.deepmedia.tools.knee.plugin.compiler.codec.Codec +import io.deepmedia.tools.knee.plugin.compiler.codec.CodegenCodecContext +import io.deepmedia.tools.knee.plugin.compiler.codec.IrCodecContext +import io.deepmedia.tools.knee.plugin.compiler.codegen.CodegenType +import io.deepmedia.tools.knee.plugin.compiler.symbols.KneeSymbols +import io.deepmedia.tools.knee.plugin.compiler.symbols.AnnotationIds +import io.deepmedia.tools.knee.plugin.compiler.utils.canonicalName +import io.deepmedia.tools.knee.plugin.compiler.utils.codegenFqName +import io.deepmedia.tools.knee.plugin.compiler.utils.simple +import org.jetbrains.kotlin.ir.builders.IrStatementsBuilder +import org.jetbrains.kotlin.ir.builders.irCall +import org.jetbrains.kotlin.ir.builders.irGet +import org.jetbrains.kotlin.ir.builders.irGetObject +import org.jetbrains.kotlin.ir.declarations.IrClass +import org.jetbrains.kotlin.ir.declarations.IrValueDeclaration +import org.jetbrains.kotlin.ir.expressions.IrExpression +import org.jetbrains.kotlin.ir.types.IrType +import org.jetbrains.kotlin.ir.types.classFqName +import org.jetbrains.kotlin.ir.types.classOrNull +import org.jetbrains.kotlin.ir.util.classIdOrFail +import org.jetbrains.kotlin.ir.util.functions +import org.jetbrains.kotlin.ir.util.hasAnnotation + +class ExportedCodec1(symbols: KneeSymbols, type: IrType, private val exportInfo: ExportInfo) : Codec( + localType = type.simple("ExportedCodec1.init"), + encodedType = when { + type.classOrNull!!.owner.hasAnnotation(AnnotationIds.KneeEnum) -> EnumCodec.encodedTypeForIr(symbols) + type.classOrNull!!.owner.hasAnnotation(AnnotationIds.KneeClass) -> ClassCodec.encodedTypeForIr(symbols) + type.classOrNull!!.owner.hasAnnotation(AnnotationIds.KneeInterface) -> InterfaceCodec.encodedTypeForIr(symbols) + else -> error("Should not happen: ${type.classFqName} not enum nor class nor interface.") + } +) { + + private val irSpec: IrClass = run { + val klass = type.classOrNull!!.owner + val fqName = when (val location = exportInfo.adapterNativeCoordinates) { + is ExportInfo.NativeCoordinates.InnerObject -> klass.classIdOrFail.createNestedClassId(location.name) + } + symbols.klass(fqName).owner + } + + private val codegenSpec: TypeName = run { + val klass = type.classOrNull!!.owner + val fqName = when (val location = exportInfo.adapterJvmCoordinates) { + is ExportInfo.JvmCoordinates.InnerObject -> klass.codegenFqName.child(location.name) + is ExportInfo.JvmCoordinates.ExternalObject -> location.parent.child(location.name) + } + CodegenType.from(fqName).name + } + + // We can't use %T format due to Codec interface design, and spec name can have a $ which must be + // enclosed in backticks in order to compile. + private val codegenSpecTypeString: String get() { + return codegenSpec.canonicalName.split(".").joinToString(".") { + if (it.contains("$")) "`$it`" else it + } + } + + override fun IrStatementsBuilder<*>.irDecode( + irContext: IrCodecContext, + jni: IrValueDeclaration + ): IrExpression { + return irCall(irSpec.functions.first { it.name.asString() == "read" }).apply { + dispatchReceiver = irGetObject(irSpec.symbol) + putValueArgument(0, irGet(irContext.environment)) + putValueArgument(1, irGet(jni)) + } + } + + override fun IrStatementsBuilder<*>.irEncode( + irContext: IrCodecContext, + local: IrValueDeclaration + ): IrExpression { + return irCall(irSpec.functions.first { it.name.asString() == "write" }).apply { + dispatchReceiver = irGetObject(irSpec.symbol) + putValueArgument(0, irGet(irContext.environment)) + putValueArgument(1, irGet(local)) + } + } + + override fun CodeBlock.Builder.codegenDecode( + codegenContext: CodegenCodecContext, + jni: String + ): String { + return "${codegenSpecTypeString}.read($jni)" + } + + override fun CodeBlock.Builder.codegenEncode( + codegenContext: CodegenCodecContext, + local: String + ): String { + return "${codegenSpecTypeString}.write($local)" + } +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/export/v2/ExportAdapters2.kt b/knee-compiler-plugin/src/main/kotlin/export/v2/ExportAdapters2.kt new file mode 100644 index 0000000..31a55b5 --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/export/v2/ExportAdapters2.kt @@ -0,0 +1,79 @@ +package io.deepmedia.tools.knee.plugin.compiler.export.v2 + +import com.squareup.kotlinpoet.CodeBlock +import com.squareup.kotlinpoet.withIndent +import io.deepmedia.tools.knee.plugin.compiler.codec.CodegenCodecContext +import io.deepmedia.tools.knee.plugin.compiler.codec.IrCodecContext +import io.deepmedia.tools.knee.plugin.compiler.context.KneeContext +import io.deepmedia.tools.knee.plugin.compiler.symbols.RuntimeIds +import io.deepmedia.tools.knee.plugin.compiler.symbols.CInteropIds +import io.deepmedia.tools.knee.plugin.compiler.symbols.PlatformIds +import io.deepmedia.tools.knee.plugin.compiler.utils.irLambda +import org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder +import org.jetbrains.kotlin.ir.builders.* +import org.jetbrains.kotlin.ir.expressions.IrConstructorCall +import org.jetbrains.kotlin.ir.types.typeWith +import org.jetbrains.kotlin.ir.util.constructors + + +object ExportAdapters2 { + + fun CodeBlock.Builder.codegenCreateExportAdapter( + info: ExportedTypeInfo, + context: KneeContext, + ) { + // Note: reverse = false but we don't relly know if the obj being converted is a param or return type + // We should reconsider this reverse flag as it does not generalize properly to export specs + val codecContext = CodegenCodecContext(null, false, context.log) + val codec = context.mapper.get(info.localIrType) + + addStatement("Adapter<%T, %T>(", info.encodedType.jvm.name, info.localCodegenType.name) + withIndent { + beginControlFlow("encoder =") + addStatement(with(codec) { this@withIndent.codegenEncode(codecContext, "it") }) + endControlFlow() + beginControlFlow(", decoder =") + addStatement(with(codec) { this@withIndent.codegenDecode(codecContext, "it") }) + endControlFlow() + } + add(")") + } + + fun DeclarationIrBuilder.irCreateExportAdapter( + info: ExportedTypeInfo, + context: KneeContext, + ): IrConstructorCall { + val adapterClass = context.symbols.klass(RuntimeIds.Adapter) + val jniEnvironmentType = context.symbols.klass(CInteropIds.CPointer).typeWith(context.symbols.typeAliasUnwrapped(PlatformIds.JNIEnvVar)) + return irCallConstructor(adapterClass.constructors.single(), listOf(info.encodedType.kn, info.localIrType)).apply { + // Encode + putValueArgument(0, irLambda( + context = context, + parent = parent, + valueParameters = listOf(jniEnvironmentType, info.localIrType), + returnType = info.encodedType.kn, + content = { lambda -> + // Note: reverse = false but we don't relly know if the obj being converted is a param or return type + // TODO: reconsider this reverse flag as it does not generalize properly to export specs + val codecContext = IrCodecContext(null, lambda.valueParameters[0], false, context.log) + val codec = context.mapper.get(info.localIrType) + with(codec) { +irReturn(irEncode(codecContext, lambda.valueParameters[1])) } + } + )) + // Decode + putValueArgument(1, irLambda( + context = context, + parent = parent, + valueParameters = listOf(jniEnvironmentType, info.encodedType.kn), + returnType = info.localIrType, + content = { lambda -> + // Note: reverse = false but we don't relly know if the obj being converted is a param or return type + // TODO: reconsider this reverse flag as it does not generalize properly to export specs + val codecContext = IrCodecContext(null, lambda.valueParameters[0], false, context.log) + val codec = context.mapper.get(info.localIrType) + with(codec) { +irReturn(irDecode(codecContext, lambda.valueParameters[1])) } + } + )) + } + } +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/export/v2/ExportedCodec2.kt b/knee-compiler-plugin/src/main/kotlin/export/v2/ExportedCodec2.kt new file mode 100644 index 0000000..607df47 --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/export/v2/ExportedCodec2.kt @@ -0,0 +1,79 @@ +package io.deepmedia.tools.knee.plugin.compiler.export.v2 + +import com.squareup.kotlinpoet.CodeBlock +import io.deepmedia.tools.knee.plugin.compiler.codec.Codec +import io.deepmedia.tools.knee.plugin.compiler.codec.CodegenCodecContext +import io.deepmedia.tools.knee.plugin.compiler.codec.IrCodecContext +import io.deepmedia.tools.knee.plugin.compiler.symbols.KneeSymbols +import io.deepmedia.tools.knee.plugin.compiler.symbols.RuntimeIds +import io.deepmedia.tools.knee.plugin.compiler.utils.asTypeName +import org.jetbrains.kotlin.ir.builders.* +import org.jetbrains.kotlin.ir.declarations.IrClass +import org.jetbrains.kotlin.ir.declarations.IrValueDeclaration +import org.jetbrains.kotlin.ir.expressions.IrExpression +import org.jetbrains.kotlin.ir.util.defaultType + +class ExportedCodec2(symbols: KneeSymbols, exportingModule: IrClass, exportedType: ExportedTypeInfo) : Codec( + localIrType = exportedType.localIrType, + localCodegenType = exportedType.localCodegenType, + encodedType = exportedType.encodedType, +) { + + private val typeId = exportedType.id + private val moduleObject = exportingModule + private val getAdapterFunction = symbols.functions(RuntimeIds.KneeModule_getExportAdapter).single() + private val adapterDecodeFunction = symbols.functions(RuntimeIds.Adapter_decode).single() + private val adapterEncodeFunction = symbols.functions(RuntimeIds.Adapter_encode).single() + + private fun IrStatementsBuilder<*>.irGetAdapter(): IrExpression { + return irCall(getAdapterFunction).apply { + dispatchReceiver = irGetObject(moduleObject.symbol) + putTypeArgument(0, encodedType.knOrNull!!) + putTypeArgument(1, localIrType) + putValueArgument(0, irInt(typeId)) + } + } + + override fun IrStatementsBuilder<*>.irDecode( + irContext: IrCodecContext, + jni: IrValueDeclaration + ): IrExpression { + return irCall(adapterDecodeFunction).apply { + dispatchReceiver = irGetAdapter() + putValueArgument(0, irGet(irContext.environment)) + putValueArgument(1, irGet(jni)) + } + } + + override fun IrStatementsBuilder<*>.irEncode( + irContext: IrCodecContext, + local: IrValueDeclaration + ): IrExpression { + return irCall(adapterEncodeFunction).apply { + dispatchReceiver = irGetAdapter() + putValueArgument(0, irGet(irContext.environment)) + putValueArgument(1, irGet(local)) + } + } + + private fun CodeBlock.Builder.addGetAdapterStatement(variableName: String) { + val module = moduleObject.defaultType.asTypeName() + addStatement("val $variableName = %T.getExportAdapter<%T, %T>($typeId)", module, encodedType.jvmOrNull!!.name, localCodegenType.name) + } + + override fun CodeBlock.Builder.codegenDecode( + codegenContext: CodegenCodecContext, + jni: String + ): String { + addGetAdapterStatement("adapter_") + return "adapter_.decode($jni)" + } + + override fun CodeBlock.Builder.codegenEncode( + codegenContext: CodegenCodecContext, + local: String + ): String { + addGetAdapterStatement("adapter_") + return "adapter_.encode($local)" + } +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/export/v2/ExportedTypeInfo.kt b/knee-compiler-plugin/src/main/kotlin/export/v2/ExportedTypeInfo.kt new file mode 100644 index 0000000..e0fc36c --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/export/v2/ExportedTypeInfo.kt @@ -0,0 +1,28 @@ +package io.deepmedia.tools.knee.plugin.compiler.export.v2 + +import io.deepmedia.tools.knee.plugin.compiler.codec.Codec +import io.deepmedia.tools.knee.plugin.compiler.codegen.CodegenType +import io.deepmedia.tools.knee.plugin.compiler.jni.JniType +import kotlinx.serialization.Contextual +import kotlinx.serialization.Serializable +import org.jetbrains.kotlin.ir.types.IrSimpleType +import org.jetbrains.kotlin.ir.types.IrType + + +@Serializable +data class ExportedTypeInfo( + val id: Int, // unique within the same module + @Contextual val localIrType: IrSimpleType, + val localCodegenType: CodegenType, + val encodedType: JniType.Real +) { + constructor(id: Int, codec: Codec) : this( + id = id, + localIrType = codec.localIrType, + localCodegenType = codec.localCodegenType, + encodedType = checkNotNull(codec.encodedType as? JniType.Real) { + "Can't export ${codec.localIrType}, its jni representation is not JniType.Real" + } + ) + // val uniqueId: Int get() = localIrType.disambiguationHash() +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/features/KneeClass.kt b/knee-compiler-plugin/src/main/kotlin/features/KneeClass.kt new file mode 100644 index 0000000..f00f355 --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/features/KneeClass.kt @@ -0,0 +1,89 @@ +package io.deepmedia.tools.knee.plugin.compiler.features + +import io.deepmedia.tools.knee.plugin.compiler.codegen.CodegenClass +import io.deepmedia.tools.knee.plugin.compiler.import.ImportInfo +import io.deepmedia.tools.knee.plugin.compiler.symbols.AnnotationIds +import io.deepmedia.tools.knee.plugin.compiler.symbols.KneeSymbols +import io.deepmedia.tools.knee.plugin.compiler.symbols.KotlinIds +import io.deepmedia.tools.knee.plugin.compiler.utils.requireNotComplex +import org.jetbrains.kotlin.descriptors.ClassKind +import org.jetbrains.kotlin.ir.declarations.* +import org.jetbrains.kotlin.ir.util.* +import org.jetbrains.kotlin.name.FqName + +class KneeClass( + source: IrClass, + val importInfo: ImportInfo? = null +) : KneeFeature(source, "KneeClass") { + + val constructors: List + val members: List + val properties: List + val isThrowable: Boolean + + init { + source.requireNotComplex( + this, ClassKind.CLASS, + typeArguments = importInfo?.type?.arguments ?: emptyList() + ) + + val allConstructors = source.constructors.toList() + val constructors = allConstructors + .filter { it.hasAnnotation(AnnotationIds.Knee) } + .takeIf { it.isNotEmpty() } + ?: emptyList() + // Removing this, people might want classes with no constructors exported. + // ?: listOf(allConstructors.single { it.isPrimary }) + + val members = source.functions + .filter { it.hasAnnotationCopyingFromParents(AnnotationIds.Knee) } + // exclude static function (see isStaticMethodOfClass impl) + // and property accessors (one should use @Knee on the property instead) + .filter { it.dispatchReceiverParameter != null } + .filter { !it.isPropertyAccessor } + .onEach { it.requireNotComplex("$this member ${it.name}", allowSuspend = true) } + .toList() + + val properties = source.properties + .filter { it.hasAnnotationCopyingFromParents(AnnotationIds.Knee) } + .toList() + + this.constructors = constructors.map { KneeDownwardFunction(it, parentInstance = this, parentProperty = null) } + this.members = members.map { KneeDownwardFunction(it, parentInstance = this, parentProperty = null) } + this.properties = properties.map { KneeDownwardProperty(it, parentInstance = this) } + this.isThrowable = source.getAllSuperclasses().any { + it.classId == KotlinIds.Throwable + } + } + + /** + * A property should have the override modifier in codegen only if it refers to some superclass, + * but the one and only superclass that we currently preserve in codegen is [kotlin.Throwable]. + */ + fun isOverrideInCodegen(symbols: KneeSymbols, property: KneeDownwardProperty): Boolean { + if (!isThrowable) return false + // NOTE: Symbol.equals() not good enough, need to compare by name + // The overridden symbol may be a FAKE_OVERRIDE unlike the one we get directly from the class + val throwableSymbols = symbols.klass(KotlinIds.Throwable).owner.properties.map { it.symbol.owner.name }.toList() + val propertyOverriddenSymbols = property.source.overriddenSymbols.map { it.owner.name } + return propertyOverriddenSymbols.any { it in throwableSymbols } + } + + @Suppress("UNCHECKED_CAST") + private fun > T.findAnnotatedParentRecursive(annotation: FqName): T? { + return overriddenSymbols.asSequence().map { + val t = it.owner as T + if (t.hasAnnotation(annotation)) return@map t + t.findAnnotatedParentRecursive(annotation) + }.firstOrNull { it != null } + } + + private fun > T.hasAnnotationCopyingFromParents(annotation: FqName): Boolean { + if (hasAnnotation(annotation)) return true + val parent = findAnnotatedParentRecursive(annotation) ?: return false + copyAnnotationsFrom(parent) + return true + } + + lateinit var codegenClone: CodegenClass +} diff --git a/knee-compiler-plugin/src/main/kotlin/features/KneeCollector.kt b/knee-compiler-plugin/src/main/kotlin/features/KneeCollector.kt new file mode 100644 index 0000000..5e1ee6d --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/features/KneeCollector.kt @@ -0,0 +1,195 @@ +package io.deepmedia.tools.knee.plugin.compiler.features + +import io.deepmedia.tools.knee.plugin.compiler.import.ImportInfo +import io.deepmedia.tools.knee.plugin.compiler.symbols.RuntimeIds +import io.deepmedia.tools.knee.plugin.compiler.symbols.AnnotationIds +import io.deepmedia.tools.knee.plugin.compiler.utils.simple +import org.jetbrains.kotlin.descriptors.ClassKind +import org.jetbrains.kotlin.ir.IrElement +import org.jetbrains.kotlin.ir.declarations.* +import org.jetbrains.kotlin.ir.expressions.IrCall +import org.jetbrains.kotlin.ir.types.classOrFail +import org.jetbrains.kotlin.ir.util.* +import org.jetbrains.kotlin.ir.util.isPropertyAccessor +import org.jetbrains.kotlin.ir.visitors.* + +class KneeCollector(module: IrModuleFragment) : IrElementVisitorVoid { + + + val initializers = mutableListOf() + val modules = mutableListOf() + var hasDeclarations = false + + private val classes = mutableListOf() + private val enums = mutableListOf() + private val interfaces = mutableListOf() + + private val importedClasses = mutableListOf() + private val importedEnums = mutableListOf() + private val importedInterfaces = mutableListOf() + + // private val imports = mutableListOf() + private val topLevelDownwardFunctions = mutableListOf() + private val topLevelDownwardProperties = mutableListOf() + + val allInterfaces get() = interfaces + importedInterfaces + // + imports.flatMap { it.interfaces } + + val allEnums get() = enums + importedEnums + // + imports.flatMap { it.enums } + + val allClasses get() = classes + importedClasses + // + imports.flatMap { it.classes } + + val allDownwardProperties get() = topLevelDownwardProperties + + allClasses.flatMap { it.properties } + + allInterfaces.flatMap { it.downwardProperties } + + val allDownwardFunctions get() = topLevelDownwardFunctions + + allDownwardProperties.flatMap { it.functions } + + allClasses.flatMap { it.functions } + + allInterfaces.flatMap { it.downwardFunctions } + + val allUpwardProperties get() = + allInterfaces.flatMap { it.upwardProperties } + + val allUpwardFunctions get() = + allUpwardProperties.flatMap { it.functions } + + allInterfaces.flatMap { it.upwardFunctions } + + private val KneeClass.functions get() = constructors + members // + disposer + private val KneeDownwardProperty.functions get() = listOfNotNull(getter, setter) + private val KneeUpwardProperty.functions get() = listOfNotNull(getter, setter) + + init { + module.acceptVoid(this) + // reconcileExpectActual(module) + } + + + /* @OptIn(ObsoleteDescriptorBasedAPI::class) + private fun reconcileExpectActual(module: IrModuleFragment) { + val allActuals = (allClasses + allDownwardFunctions) + .filter { (it.source.descriptor as MemberDescriptor).isActual } + .associateWith { + val expects = it.source.descriptor.findExpects() + check(expects.isNotEmpty()) { "$it marked as `actual` but could not find any corresponding `expect` declaration." } + expects + } + val frontendToIr: MutableMap = allActuals + .flatMap { it.value } + .associateWithTo(mutableMapOf()) { null } + module.acceptVoid(object : IrElementVisitorVoid { + override fun visitElement(element: IrElement) { + element.acceptChildrenVoid(this) + } + override fun visitClass(declaration: IrClass) { + if (declaration.descriptor in frontendToIr.keys) frontendToIr[declaration.descriptor] = declaration + super.visitClass(declaration) + } + override fun visitSimpleFunction(declaration: IrSimpleFunction) { + if (declaration.descriptor in frontendToIr.keys) frontendToIr[declaration.descriptor] = declaration + super.visitSimpleFunction(declaration) + } + }) + + allActuals.forEach { (knee, descriptors) -> + knee.expectSources = descriptors.map { + frontendToIr[it] ?: error("Could not find `actual` $it for Knee type $knee.") + } + } + } */ + + override fun visitElement(element: IrElement) { + element.acceptChildrenVoid(this) + } + + override fun visitSimpleFunction(declaration: IrSimpleFunction) { + /* if (declaration.hasAnnotation(kneeInitAnnotation)) { + inits.add(KneeInit(declaration)) + } else */if (declaration.hasAnnotation(AnnotationIds.Knee) + && declaration.isTopLevel + && !declaration.isPropertyAccessor) { + hasDeclarations = true + topLevelDownwardFunctions.add(KneeDownwardFunction(declaration, null, null)) + } + super.visitSimpleFunction(declaration) + } + + override fun visitTypeAlias(declaration: IrTypeAlias) { + if (declaration.hasAnnotation(AnnotationIds.KneeEnum)) { + hasDeclarations = true + val importInfo = ImportInfo(declaration.expandedType.simple("visitTypeAlias"), declaration) + importedEnums.add(KneeEnum(declaration.expandedType.classOrFail.owner, importInfo)) + } else if (declaration.hasAnnotation(AnnotationIds.KneeClass)) { + hasDeclarations = true + val importInfo = ImportInfo(declaration.expandedType.simple("visitTypeAlias"), declaration) + importedClasses.add(KneeClass(declaration.expandedType.classOrFail.owner, importInfo)) + } else if (declaration.hasAnnotation(AnnotationIds.KneeInterface)) { + hasDeclarations = true + val importInfo = ImportInfo(declaration.expandedType.simple("visitTypeAlias"), declaration) + importedInterfaces.add(KneeInterface(declaration.expandedType.classOrFail.owner, importInfo)) + } + super.visitTypeAlias(declaration) + } + + override fun visitClass(declaration: IrClass) { + if (declaration.hasAnnotation(AnnotationIds.KneeEnum)) { + hasDeclarations = true + enums.add(KneeEnum(declaration)) + } else if (declaration.hasAnnotation(AnnotationIds.KneeClass)) { + hasDeclarations = true + classes.add(KneeClass(declaration)) + } else if (declaration.hasAnnotation(AnnotationIds.KneeInterface)) { + hasDeclarations = true + interfaces.add(KneeInterface(declaration)) + } /* else if (declaration.hasAnnotation(kneeImportAnnotation)) { + imports.add(KneeImport(declaration).also { + hasDeclarations = hasDeclarations || it.classes.isNotEmpty() || it.enums.isNotEmpty() || it.interfaces.isNotEmpty() + }) + } */ else if (declaration.kind == ClassKind.OBJECT && declaration.superClass?.classId == RuntimeIds.KneeModule) { + modules.add(KneeModule(declaration)) + }/* else if ((declaration.descriptor as? MemberDescriptor)?.isActual == true) { + allActualClasses.add(declaration) + }*/ + super.visitClass(declaration) + } + + override fun visitCall(expression: IrCall) { + // Some functions throw at .callableId + val callableId = runCatching { expression.symbol.owner.callableId }.getOrNull() + if (callableId == RuntimeIds.initKnee) { + initializers.add(KneeInitializer(expression)) + } + super.visitCall(expression) + } + + override fun visitProperty(declaration: IrProperty) { + if (declaration.isTopLevel) { + if (declaration.hasAnnotation(AnnotationIds.Knee)) { + hasDeclarations = true + topLevelDownwardProperties.add(KneeDownwardProperty(declaration, null)) + } else { + // Old KneeModule detection + /* val type = declaration.backingField?.type as? IrSimpleType + if (type?.classOrNull?.owner?.classId == Names.runtimeKneeModuleClass) { + val initializer = declaration.backingField!!.initializer?.expression + if (initializer is IrConstructorCall) { + val publicConstructor = type.classOrFail.constructors.first { !it.owner.isPrimary } + if (initializer.symbol == publicConstructor) { + modules.add(KneeModule(declaration, initializer)) + } + } + } */ + } + } + super.visitProperty(declaration) + } + + /* override fun visitTypeAlias(declaration: IrTypeAlias) { + if (declaration.isActual) { + allActualTypeAliases.add(declaration) + } + super.visitTypeAlias(declaration) + } */ +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/features/KneeDownwardFunction.kt b/knee-compiler-plugin/src/main/kotlin/features/KneeDownwardFunction.kt new file mode 100644 index 0000000..786ba16 --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/features/KneeDownwardFunction.kt @@ -0,0 +1,53 @@ +package io.deepmedia.tools.knee.plugin.compiler.features + +import io.deepmedia.tools.knee.plugin.compiler.import.ImportInfo +import io.deepmedia.tools.knee.plugin.compiler.utils.* +import org.jetbrains.kotlin.descriptors.ClassKind +import org.jetbrains.kotlin.ir.declarations.IrClass +import org.jetbrains.kotlin.ir.declarations.IrConstructor +import org.jetbrains.kotlin.ir.declarations.IrFunction +import org.jetbrains.kotlin.ir.util.* + +class KneeDownwardFunction( + source: IrFunction, + parentInstance: KneeFeature<*>?, // class or interface + parentProperty: KneeDownwardProperty? +) : KneeFeature(source, "Knee") { + + sealed class Kind(val property: KneeDownwardProperty?) { + + class TopLevel(property: KneeDownwardProperty?) : Kind(property) + + class ClassConstructor(val owner: KneeClass) : Kind(null) + + class ClassMember(val owner: KneeClass, property: KneeDownwardProperty?) : Kind(property) + + class InterfaceMember(val owner: KneeInterface, property: KneeDownwardProperty?) : Kind(property) + + val importInfo: ImportInfo? get() = when (this) { + is TopLevel, is ClassConstructor, is ClassMember -> null + is InterfaceMember -> owner.importInfo + } + } + + val kind: Kind = when { + source.isTopLevel -> Kind.TopLevel(parentProperty) + source is IrConstructor -> Kind.ClassConstructor(parentInstance as KneeClass) + // source.name == referenceDisposerName() -> Kind.ClassDisposer + source.dispatchReceiverParameter != null + && source.parent is IrClass + && source.parentAsClass.kind == ClassKind.CLASS -> { + Kind.ClassMember(parentInstance as KneeClass, parentProperty) + } + source.dispatchReceiverParameter != null + && source.parent is IrClass + && source.parentAsClass.kind == ClassKind.INTERFACE -> { + Kind.InterfaceMember(parentInstance as KneeInterface, parentProperty) + } + else -> error("$this must be top level, a class constructor, a class destructor or a class member.") + } + + init { + source.requireNotComplex(this, allowSuspend = true) + } +} diff --git a/knee-compiler-plugin/src/main/kotlin/features/KneeDownwardProperty.kt b/knee-compiler-plugin/src/main/kotlin/features/KneeDownwardProperty.kt new file mode 100644 index 0000000..b831138 --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/features/KneeDownwardProperty.kt @@ -0,0 +1,43 @@ +package io.deepmedia.tools.knee.plugin.compiler.features + +import io.deepmedia.tools.knee.plugin.compiler.codegen.CodegenProperty +import io.deepmedia.tools.knee.plugin.compiler.import.ImportInfo +import org.jetbrains.kotlin.ir.declarations.IrProperty +import org.jetbrains.kotlin.ir.util.copyAnnotationsFrom + +class KneeDownwardProperty( + source: IrProperty, + parentInstance: KneeFeature<*>? +) : KneeFeature(source, "Knee") { + + sealed class Kind { + class InterfaceMember(val owner: KneeInterface) : Kind() + class ClassMember(val owner: KneeClass) : Kind() + object TopLevel : Kind() + + val importInfo: ImportInfo? get() = when (this) { + TopLevel -> null + is ClassMember -> owner.importInfo + is InterfaceMember -> owner.importInfo + } + } + + val kind = when (parentInstance) { + is KneeInterface -> Kind.InterfaceMember(parentInstance) + is KneeClass -> Kind.ClassMember(parentInstance) + else -> Kind.TopLevel + } + + + val setter: KneeDownwardFunction? = source.setter?.let { + it.copyAnnotationsFrom(source) + KneeDownwardFunction(it, parentInstance, this) + } + + val getter: KneeDownwardFunction = requireNotNull(source.getter) { "$this must have a getter." }.let { + it.copyAnnotationsFrom(source) + KneeDownwardFunction(it, parentInstance, this) + } + + lateinit var codegenImplementation: CodegenProperty +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/features/KneeEnum.kt b/knee-compiler-plugin/src/main/kotlin/features/KneeEnum.kt new file mode 100644 index 0000000..a548894 --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/features/KneeEnum.kt @@ -0,0 +1,31 @@ +package io.deepmedia.tools.knee.plugin.compiler.features + +import io.deepmedia.tools.knee.plugin.compiler.import.ImportInfo +import io.deepmedia.tools.knee.plugin.compiler.utils.requireNotComplex +import org.jetbrains.kotlin.descriptors.ClassKind +import org.jetbrains.kotlin.ir.IrElement +import org.jetbrains.kotlin.ir.declarations.IrClass +import org.jetbrains.kotlin.ir.declarations.IrEnumEntry +import org.jetbrains.kotlin.ir.visitors.IrElementVisitorVoid +import org.jetbrains.kotlin.ir.visitors.acceptChildrenVoid + +class KneeEnum( + source: IrClass, + val importInfo: ImportInfo? = null +) : KneeFeature(source, "KneeEnum") { + + val entries: List + + init { + source.requireNotComplex(this, ClassKind.ENUM_CLASS) + val entries = mutableListOf() + source.acceptChildrenVoid(object : IrElementVisitorVoid { + override fun visitElement(element: IrElement) = Unit + override fun visitEnumEntry(declaration: IrEnumEntry) { + entries.add(declaration) + super.visitEnumEntry(declaration) + } + }) + this.entries = entries + } +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/features/KneeFeature.kt b/knee-compiler-plugin/src/main/kotlin/features/KneeFeature.kt new file mode 100644 index 0000000..fb770a8 --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/features/KneeFeature.kt @@ -0,0 +1,44 @@ +package io.deepmedia.tools.knee.plugin.compiler.features + +import io.deepmedia.tools.knee.plugin.compiler.codegen.CodegenDeclaration +import org.jetbrains.kotlin.ir.declarations.IrDeclaration +import org.jetbrains.kotlin.ir.declarations.IrDeclarationWithName +import org.jetbrains.kotlin.ir.util.* + +abstract class KneeFeature( + val source: Ir, + private val annotation: String, +) { + + var expectSources: List = emptyList() + + init { + require(source.fileOrNull?.getPackageFragment()?.packageFqName?.isRoot != true) { + "$this can't be in root package." + } + requireNotNull(source.fqNameWhenAvailable) { + "$this must have a fully qualified name." + } + check(!source.isExpect) { + "$this can't be an `expect` type." + } + } + + + val irProducts = mutableListOf() + val codegenProducts = mutableListOf>() + + fun dump(rawIr: Boolean = false): String { + val hasProducts = irProducts.isNotEmpty() || codegenProducts.isNotEmpty() + return if (!hasProducts) { + if (rawIr) source.dump() else source.dumpKotlinLike() + } else { + val ir = irProducts.map { if (rawIr) it.dump() else it.dumpKotlinLike() } + val codegen = codegenProducts.map { it.toString() } + listOf("IR (${ir.size})", *ir.toTypedArray(), "CODEGEN (${codegen.size})", *codegen.toTypedArray()) + .joinToString(separator = "\n") + } + } + + final override fun toString() = "@$annotation " + (source.fqNameWhenAvailable?.asString() ?: source.name.asString()) +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/features/KneeImport.kt b/knee-compiler-plugin/src/main/kotlin/features/KneeImport.kt new file mode 100644 index 0000000..1e83b4e --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/features/KneeImport.kt @@ -0,0 +1,46 @@ +package io.deepmedia.tools.knee.plugin.compiler.features + +/* class KneeImport(source: IrClass) : KneeFeature(source, "KneeImport") { + + val interfaces: List + val enums: List + val classes: List + + init { + val interfaces = mutableListOf() + val enums = mutableListOf() + val classes = mutableListOf() + source.requireNotComplex(this, ClassKind.INTERFACE) + source.acceptChildrenVoid(object : IrElementVisitorVoid { + override fun visitElement(element: IrElement) = Unit + + override fun visitProperty(declaration: IrProperty) { + if (declaration.hasAnnotation(kneeInterfaceAnnotation)) { + val type = (declaration.backingField?.type ?: declaration.getter?.returnType)!! as IrSimpleType + val info = ImportInfo(type, declaration) + interfaces.add(KneeInterface( + source = type.classOrNull!!.owner, + importInfo = info + )) + } else if (declaration.hasAnnotation(kneeEnumAnnotation)) { + val type = (declaration.backingField?.type ?: declaration.getter?.returnType)!! as IrSimpleType + val info = ImportInfo(type, declaration) + enums.add(KneeEnum( + source = type.classOrNull!!.owner, + importInfo = info + )) + }else if (declaration.hasAnnotation(kneeClassAnnotation)) { + val type = (declaration.backingField?.type ?: declaration.getter?.returnType)!! as IrSimpleType + val info = ImportInfo(type, declaration) + classes.add(KneeClass( + source = type.classOrNull!!.owner, + importInfo = info + )) + } + } + }) + this.interfaces = interfaces + this.enums = enums + this.classes = classes + } +} */ diff --git a/knee-compiler-plugin/src/main/kotlin/features/KneeInitializer.kt b/knee-compiler-plugin/src/main/kotlin/features/KneeInitializer.kt new file mode 100644 index 0000000..1651b9b --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/features/KneeInitializer.kt @@ -0,0 +1,49 @@ +package io.deepmedia.tools.knee.plugin.compiler.features + +import io.deepmedia.tools.knee.plugin.compiler.context.KneeContext +import org.jetbrains.kotlin.ir.expressions.IrCall +import org.jetbrains.kotlin.ir.expressions.IrExpression +import org.jetbrains.kotlin.ir.types.IrType +import org.jetbrains.kotlin.ir.util.dumpKotlinLike + +/* class KneeInit(source: IrSimpleFunction) : KneeFeature(source, "KneeInit") { + + init { + source.requireNotComplex(this) + require(source.isTopLevel) { "$this must be a top level function." } + } +} + +fun KneeInit.validate(context: KneeContext) { + val returnType = source.returnType + val expectedReturnType = context.symbols.builtIns.unitType + require(returnType == expectedReturnType) { + "$this must return ${expectedReturnType.dumpKotlinLike()} (not ${returnType.dumpKotlinLike()})" + } + val arg0 = source.valueParameters.firstOrNull()?.type + val expectedArg0 = context.symbols.jniEnvironmentType + require(arg0 == expectedArg0) { + "$this first parameter must be ${(expectedArg0 as IrType).dumpKotlinLike()} (not ${arg0?.dumpKotlinLike()})" + } +} + + +private fun DeclarationIrBuilder.irInitLambdas(context: KneeContext, inits: List): IrExpression { + val symbols = context.symbols + val type = symbols.klass(functionXInterface(1)).typeWith(symbols.jniEnvironmentType, symbols.builtIns.unitType) + return irListOf(symbols, type, inits.map { init -> + irLambda( + context = context, + parent = this@irInitLambdas.parent, + valueParameters = listOf(symbols.jniEnvironmentType), + returnType = symbols.builtIns.unitType, + content = { lambda -> + val environment = irGet(lambda.valueParameters[0]) + +irCall(init.source).apply { putValueArgument(0, environment) } + } + ) + }) +}*/ + + +class KneeInitializer(val expression: IrCall) \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/features/KneeInterface.kt b/knee-compiler-plugin/src/main/kotlin/features/KneeInterface.kt new file mode 100644 index 0000000..20907b6 --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/features/KneeInterface.kt @@ -0,0 +1,73 @@ +package io.deepmedia.tools.knee.plugin.compiler.features + +import io.deepmedia.tools.knee.plugin.compiler.codegen.CodegenClass +import io.deepmedia.tools.knee.plugin.compiler.functions.UpwardFunctionSignature +import io.deepmedia.tools.knee.plugin.compiler.import.ImportInfo +import io.deepmedia.tools.knee.plugin.compiler.utils.requireNotComplex +import org.jetbrains.kotlin.descriptors.ClassKind +import org.jetbrains.kotlin.descriptors.Modality +import org.jetbrains.kotlin.ir.builders.IrBuilderWithScope +import org.jetbrains.kotlin.ir.declarations.* +import org.jetbrains.kotlin.ir.expressions.IrExpression +import org.jetbrains.kotlin.ir.util.* + + +class KneeInterface( + source: IrClass, + val importInfo: ImportInfo? = null +) : KneeFeature(source, "KneeInterface") { + + val downwardFunctions: List + val upwardFunctions: List + + val downwardProperties: List + val upwardProperties: List + + init { + source.requireNotComplex(this, ClassKind.INTERFACE, + typeArguments = importInfo?.type?.arguments ?: emptyList() + ) + + val members = source.functions + .filter { it.dispatchReceiverParameter != null } // drop static functions + .filter { !it.isPropertyAccessor } // drop property getters and setters + // .filter { it.isFakeOverride } // drop equals, hashCode, toString + // ^ We can't do this. A function declared in some parent appears in this class as a fake override + // Just like equals, hashCode and toString do. A better filter is to take only abstract functions. + .filter { it.modality == Modality.ABSTRACT } + .onEach { it.requireNotComplex("$this member ${it.name}", allowSuspend = true) } + .toList() + + val properties = source.properties + .toList() + + this.downwardFunctions = members.map { KneeDownwardFunction(it, parentInstance = this, parentProperty = null) } + this.downwardProperties = properties.map { KneeDownwardProperty(it, parentInstance = this) } + this.upwardFunctions = members.map { KneeUpwardFunction(it, parentInterface = this) } + this.upwardProperties = properties.map { KneeUpwardProperty(it, parentInterface = this) } + } + + /** + * The interface implementation generated by Interfaces.kt. + */ + lateinit var irImplementation: IrClass + lateinit var codegenImplementation: CodegenClass + var codegenClone: CodegenClass? = null + + lateinit var irGetVirtualMachine: IrBuilderWithScope.() -> IrExpression + lateinit var irGetMethodOwner: IrBuilderWithScope.() -> IrExpression + lateinit var irGetJvmObject: IrBuilderWithScope.() -> IrExpression + lateinit var irGetMethod: IrBuilderWithScope.(UpwardFunctionSignature) -> IrExpression + + // This was written thinking that super class functions are not present in the interface if it doesn't redeclare them. + // That's not true, the simple 'functions' helper is already enough so there's nothing to do. + /* private fun IrClass.allFunctions(reject: MutableSet = mutableSetOf()): Sequence { + return sequence { + val own = functions.filter { it !in reject }.toMutableList() + yieldAll(own) + reject.addAll(own.flatMap { it.allOverridden() }) + val superClasses = superTypes.mapNotNull { it.classOrNull?.owner } + superClasses.forEach { yieldAll(it.allFunctions(reject)) } + } + } */ +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/features/KneeModule.kt b/knee-compiler-plugin/src/main/kotlin/features/KneeModule.kt new file mode 100644 index 0000000..6498b3a --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/features/KneeModule.kt @@ -0,0 +1,20 @@ +package io.deepmedia.tools.knee.plugin.compiler.features + +import io.deepmedia.tools.knee.plugin.compiler.symbols.RuntimeIds +import io.deepmedia.tools.knee.plugin.compiler.utils.requireNotComplex +import org.jetbrains.kotlin.descriptors.ClassKind +import org.jetbrains.kotlin.ir.declarations.IrClass +import org.jetbrains.kotlin.ir.util.classId +import org.jetbrains.kotlin.ir.util.isTopLevel +import org.jetbrains.kotlin.ir.util.superClass + +class KneeModule(source: IrClass) : KneeFeature(source, "KneeModule") { + init { + source.requireNotComplex(this, ClassKind.OBJECT) + requireNotNull(source.superClass?.takeIf { it.classId == RuntimeIds.KneeModule }) { + "$this must extend KneeModule." + } + require(source.visibility.isPublicAPI) { "$this must be a public, top-level object." } + require(source.isTopLevel) { "$this must be a public, top-level object." } + } +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/features/KneeUpwardFunction.kt b/knee-compiler-plugin/src/main/kotlin/features/KneeUpwardFunction.kt new file mode 100644 index 0000000..026e5ce --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/features/KneeUpwardFunction.kt @@ -0,0 +1,47 @@ +package io.deepmedia.tools.knee.plugin.compiler.features + +import io.deepmedia.tools.knee.plugin.compiler.functions.UpwardFunctionSignature +import io.deepmedia.tools.knee.plugin.compiler.import.ImportInfo +import io.deepmedia.tools.knee.plugin.compiler.utils.requireNotComplex +import org.jetbrains.kotlin.descriptors.ClassKind +import org.jetbrains.kotlin.ir.declarations.IrClass +import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction +import org.jetbrains.kotlin.ir.util.isOverridable +import org.jetbrains.kotlin.ir.util.parentAsClass + +/** + * source is an abstract function belonging to some interface. + * The implementation will be added to some "Impl" class. + */ +class KneeUpwardFunction( + source: IrSimpleFunction, + parentInterface: KneeInterface?, +) : KneeFeature(source, "Knee⬆") { + + /** + * Read [UpwardFunctionSignature] for more info. + */ + sealed class Kind { + class InterfaceMember(val parent: KneeInterface) : Kind() + + val importInfo: ImportInfo? get() = when (this) { + is InterfaceMember -> parent.importInfo + } + } + + val kind: Kind = Kind.InterfaceMember(parentInterface!!) + + init { + source.requireNotComplex(this, allowSuspend = true) + require(source.isOverridable) { "$this is not overridable." } + require(source.parent is IrClass && source.parentAsClass.kind == ClassKind.INTERFACE) { + "$this is not member of an interface." + } + } + + /** + * The generated implementation. Might be set beforehand for example by reverse property handling. + * If present, we shouldn't generate a new one of course. + */ + var implementation: IrSimpleFunction? = null +} diff --git a/knee-compiler-plugin/src/main/kotlin/features/KneeUpwardProperty.kt b/knee-compiler-plugin/src/main/kotlin/features/KneeUpwardProperty.kt new file mode 100644 index 0000000..6e63bdb --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/features/KneeUpwardProperty.kt @@ -0,0 +1,31 @@ +package io.deepmedia.tools.knee.plugin.compiler.features + +import io.deepmedia.tools.knee.plugin.compiler.import.ImportInfo +import org.jetbrains.kotlin.ir.declarations.IrProperty +import org.jetbrains.kotlin.ir.util.copyAnnotationsFrom + +class KneeUpwardProperty( + source: IrProperty, + parentInterface: KneeInterface? +) : KneeFeature(source, "Knee⬆") { + + sealed class Kind { + class InterfaceMember(val parent: KneeInterface) : Kind() + + val importInfo: ImportInfo? get() = when (this) { + is InterfaceMember -> parent.importInfo + } + } + + val kind = Kind.InterfaceMember(parentInterface!!) + + val setter: KneeUpwardFunction? = source.setter?.let { + it.copyAnnotationsFrom(source) + KneeUpwardFunction(it, parentInterface) + } + + val getter: KneeUpwardFunction = requireNotNull(source.getter) { "$this must have a getter." }.let { + it.copyAnnotationsFrom(source) + KneeUpwardFunction(it, parentInterface) + } +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/functions/DownwardFunctionSignature.kt b/knee-compiler-plugin/src/main/kotlin/functions/DownwardFunctionSignature.kt new file mode 100644 index 0000000..ded8b26 --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/functions/DownwardFunctionSignature.kt @@ -0,0 +1,256 @@ +package io.deepmedia.tools.knee.plugin.compiler.functions + +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import com.squareup.kotlinpoet.TypeName +import com.squareup.kotlinpoet.UNIT +import io.deepmedia.tools.knee.plugin.compiler.codec.* +import io.deepmedia.tools.knee.plugin.compiler.codegen.CodegenType +import io.deepmedia.tools.knee.plugin.compiler.codegen.KneeCodegen +import io.deepmedia.tools.knee.plugin.compiler.context.KneeContext +import io.deepmedia.tools.knee.plugin.compiler.features.KneeDownwardFunction.Kind +import io.deepmedia.tools.knee.plugin.compiler.features.KneeDownwardFunction +import io.deepmedia.tools.knee.plugin.compiler.import.concrete +import io.deepmedia.tools.knee.plugin.compiler.jni.JniSignature +import io.deepmedia.tools.knee.plugin.compiler.jni.JniType +import io.deepmedia.tools.knee.plugin.compiler.utils.* +import io.deepmedia.tools.knee.plugin.compiler.symbols.CInteropIds +import io.deepmedia.tools.knee.plugin.compiler.symbols.PlatformIds +import io.deepmedia.tools.knee.plugin.compiler.symbols.RuntimeIds.KneeSuspendInvoker +import org.jetbrains.kotlin.ir.declarations.IrConstructor +import org.jetbrains.kotlin.ir.declarations.IrFunction +import org.jetbrains.kotlin.ir.declarations.IrModuleFragment +import org.jetbrains.kotlin.ir.types.* +import org.jetbrains.kotlin.ir.util.* +import org.jetbrains.kotlin.name.Name + + +/** + * Represents the signature of a user defined K/N function which is supposed to be called from JVM. + * - [Kind.TopLevel]: a top level KN function + * - [Kind.ClassConstructor]: constructor of some KN class + * - [Kind.ClassMember]: member of some KN class + * - [Kind.InterfaceMember]: member of some KN interface + * + * The function bridging mechanism is pretty complex - look at [KneeDownwardFunction] for source code info. + * The gist is that we have to execute code before JNI is invoked and after the JNI result is received, + * on both ends of the function (KN - JVM). + * - JVM: encodes the parameters in a JNI friendly [JniType] + * - JVM: invokes the JNI function + * - KN: receives the JNI function call + * - KN: decodes the JNI parameters in some KN-local [IrType] + * - KN: executes the original user-defined function + * - KN: encodes the result in a JNI friendly [JniType] + * - JVM: receives the result and decodes it in some JVM-local [CodegenType] + * The existence of all these steps means that some wrapper functions must be generated where we can + * executed the encode / decode code, as defined by the [Codec] associated with the type that must cross JNI. + * + * So we have to generate: + * 1. Some 'external' function on the JVM side. This function might have extra parameters as per [extraParameters] + * 2. Some wrapper function on the KN side. This function might have extra parameters as per [extraParameters] + * and also has jni prefix parameters as per [knPrefixParameters] + * Note that extras are added by us to ease the function execution, like in class members or suspend functions. + * + * In addition, things get more complex because of the @KneeRaw annotation, here represented by + * the [DownwardFunctionSignature.RawKind] interface: + * - If it specifies a number, that number is the index of the [knPrefixParameters] parameter which should be + * copied into that parameter. Can be used for example to access the environment or the jobject / jclass. + * Such copying parameters are exposed in [knCopyParameters]. + * - If it specifies a fully qualified name, that's the name of a JVM-specific class which doesn't exist + * in K/N, that we want to map with that specific raw parameter. For example, we might want to get + * a Surface from Android as a raw jobject which can be later passed to the Android NDK APIs. + * Such parameters are exposed in [regularParameters] together will all other params. + * + * This class tries to expose the [Codec]s where possible, so that consumers can choose which type to use + * and where / when. + */ +class DownwardFunctionSignature(source: IrFunction, kind: Kind, context: KneeContext) { + + object Extra { + // kotlinpoet has a rule for which args must start with lowercase letter + val SuspendInvoker = Name.identifier("suspendInvoker__") + val ReceiverInstance = Name.identifier("instance__") + } + + object KnPrefix { + val JniEnvironment = Name.identifier("__jniEnvironment") + val JniObjectOrClass = Name.identifier("__jniObjectOrClass") + } + + val isSuspend = source.isSuspend + + val result: Codec = run { + val type = source.returnType.simple("DownwardSignature.result").concrete(kind.importInfo) + val codec = context.mapper.get(type, source) + when { + !isSuspend -> codec + else -> GenericCodec(context.symbols, codec) + } + } + + val suspendResult: Codec = context.mapper.get(context.symbols.builtIns.longType) + + val extraParameters: List> = buildList { + // instance member functions should pass the handle reference so that we can decode them in IR. + if (kind is Kind.ClassMember || kind is Kind.InterfaceMember) { + add(Extra.ReceiverInstance to context.mapper.get(source.parentAsClass.thisReceiver!!.type.simple("DownwardSignature.extraParams").concrete(kind.importInfo))) + } + // suspend functions should pass the 'continuation'. It is an instance of KneeSuspendInvoker + // note that encoded type might very well be Unit if the function returned void + if (isSuspend) { + add(Extra.SuspendInvoker to IdentityCodec(JniType.Object( + symbols = context.symbols, + jvm = CodegenType.from(ClassName + .bestGuess(KneeSuspendInvoker.asString()) + .parameterizedBy(result.encodedType.jvmOrNull?.name ?: UNIT)) + ))) + } + } + + val regularParameters: List> = source.valueParameters.map { + it.name to context.mapper.get(it.type.simple("DownwardSignature.regularParams").concrete(kind.importInfo), it) + /* it.name to when (val rawKind = it.rawKind) { + null -> mapper.get(it.type, kind.importInfo) + // is RawKind.CopyAtIndex -> null + is RawKind.Class -> { + // TODO: it should be possible to include generics in fqName, we should parse them + val jobject = JniType.Object(context.symbols, CodegenType.from(rawKind.fqName)) + require(jobject.kn.makeNullable() == it.type.makeNullable()) { + "@KneeRaw(${rawKind.fqName}) should be applied on a parameter of type 'jobject' or similar." + } + IdentityCodec(type = jobject) + } + } */ + } + + /** + * The IR bridge function has extra parameters that we call "prefix" parameters. These are: + * - a pointer to the JNI environment, JNIEnv* + * - a jobject or jclass depending on the type of function (member vs static) + */ + val knPrefixParameters: List> = buildList { + // pointer to jni env + add(KnPrefix.JniEnvironment to context.symbols.klass(CInteropIds.CPointer) + .typeWith(context.symbols.typeAliasUnwrapped(PlatformIds.JNIEnvVar))) + // jobject or jclass depending on static vs instance function. In practice this won't make + // any difference because jclass is a typealias for jobject, but whatever. + add(KnPrefix.JniObjectOrClass to context.symbols.typeAliasUnwrapped(when (kind) { + is Kind.TopLevel, + is Kind.ClassConstructor -> PlatformIds.jobject + is Kind.ClassMember -> PlatformIds.jclass + is Kind.InterfaceMember -> PlatformIds.jclass + })) + } + + /** + * Unsubstituted meaning that we don't pass importInfo, so generics are preserved as raw types. + * One could just use type.asTypeName() but we must pass through the mapper for some edge scenarios, + * like @KneeRaw-annotated declarations or other things. + */ + val unsubstitutedValueParametersForCodegen: List> = source.valueParameters.map { + it.name to (runCatching { context.mapper.get(it.type, it) }.getOrNull()?.localCodegenType?.name ?: it.type.simple("DownwardSignature.valueParams").asTypeName()) + } + + /** + * Unsubstituted meaning that we don't pass importInfo, so generics are preserved as raw types. + * One could just use type.asTypeName() but we must pass through the mapper for some edge scenarios, + * like @KneeRaw-annotated declarations or other things. + */ + val unsubstitutedReturnTypeForCodegen: TypeName = run { + runCatching { context.mapper.get(source.returnType, source) }.getOrNull()?.localCodegenType?.name ?: source.returnType.simple("DownwardSignature.valueParams").asTypeName() + } + + /** + * Note: [Int] here is the index to be copied in the whole array, including prefixes and extras. + * The [Name] can be used to identify the position of this parameter in the user defined function. + */ + /* val knCopyParameters: List> = source.valueParameters.mapNotNull { + when (val rawKind = it.rawKind) { + is RawKind.CopyAtIndex -> it.name to rawKind.index + is RawKind.Class -> null + else -> null + } + } */ + + /* @Suppress("UNCHECKED_CAST") + private val IrValueParameter.rawKind: RawKind? get() { + val annotation = getAnnotation(Names.kneeRawAnnotation) ?: return null + val content = (annotation.getValueArgument(0)!! as IrConst).value + return RawKind.Class(content) + /* return when (val int = content.toIntOrNull()) { + null -> RawKind.Class(content) + else -> RawKind.CopyAtIndex(int) + } */ + } */ + + /** + * Arguments of @Knee annotated functions can use @KneeRaw to say that + * they want to receive one of the copy parameters or a raw class. + */ + /* private sealed class RawKind { + // data class CopyAtIndex(val index: Int) : RawKind() + data class Class(val fqName: String) : RawKind() + } */ + + val jniInfo = JniInfo(source, kind, context.module) + + // Used for registerNatives / codegen + inner class JniInfo internal constructor( + private val source: IrFunction, + private val kind: Kind, + module: IrModuleFragment + ) { + + val owner: CodegenType by lazy { + when (kind) { + is Kind.InterfaceMember -> kind.owner.codegenImplementation.type + is Kind.ClassMember -> kind.owner.codegenClone.type + is Kind.ClassConstructor -> { + // Technically for constructors we codegen in the companion object, but it makes no difference + // from the JVM perspective, it's a function inside the owner class. + kind.owner.codegenClone.type + } + is Kind.TopLevel -> { + val packageName = (kind.importInfo?.file ?: source.file).packageFqName + val className = "${KneeCodegen.Filename}Kt" + CodegenType.from("$packageName.$className") + } + } + } + + @Suppress("DefaultLocale") + fun name(includeAncestors: Boolean): Name { + + // when ancestors are required for higher disambiguation, we must include the importInfo id. + val suffix = source.valueParameters.makeFunctionNameDisambiguationSuffix() + val prefix = kind.importInfo?.id?.takeIf { includeAncestors } + + fun mapper(name: String): String = "$" + when (kind) { + is Kind.TopLevel, + is Kind.ClassMember, + is Kind.InterfaceMember -> listOfNotNull(prefix, name, suffix).joinToString(separator = "_") + is Kind.ClassConstructor -> { + // standard name for constructors is for all of them, so we must make it unique in some way. + val index = source.parentAsClass.constructors.toList().indexOf(source as IrConstructor) + listOfNotNull(prefix, "${name}${index}").joinToString(separator = "_") + } + } + // Since this is used in codegen, it can't be a special name + return when { + includeAncestors -> source.codegenUniqueName(false, ::mapper) + else -> source.codegenName.map(false, ::mapper) + } + } + + val signature: String by lazy { + val returnType: JniType = (if (isSuspend) suspendResult else result).encodedType + val extraTypes: List = extraParameters.map { (_, codec) -> codec.encodedType } + val actualTypes: List = regularParameters.map { (_, codec) -> codec.encodedType } + JniSignature.get( + returnType = returnType, + argumentTypes = extraTypes + actualTypes + ) + } + } + +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/functions/DownwardFunctionsCodegen.kt b/knee-compiler-plugin/src/main/kotlin/functions/DownwardFunctionsCodegen.kt new file mode 100644 index 0000000..afde93a --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/functions/DownwardFunctionsCodegen.kt @@ -0,0 +1,96 @@ +package io.deepmedia.tools.knee.plugin.compiler.functions + +import com.squareup.kotlinpoet.CodeBlock +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.KModifier +import com.squareup.kotlinpoet.UNIT +import io.deepmedia.tools.knee.plugin.compiler.codec.CodegenCodecContext +import io.deepmedia.tools.knee.plugin.compiler.utils.asStringSafeForCodegen +import org.jetbrains.kotlin.name.Name + +/** + * Codegen companion of [DownwardFunctionsIr]. + */ +object DownwardFunctionsCodegen { + + /** + * Calls jni from the local function. The difference with the IR counterpart of this function + * is that the jni/bridge function does not exist and we must generate it here (and return it). + */ + fun CodeBlock.Builder.codegenInvoke( + signature: DownwardFunctionSignature, + bridgeFunctionName: Name, + prefix: String, // e.g. "return " + codecContext: CodegenCodecContext, + ): FunSpec.Builder { + + // Create the bridge, jni external function + // println("codegenLocalToJni: $bridgeFunctionName") + val bridgeName = bridgeFunctionName.asString() + val bridgeSpec = FunSpec + .builder(bridgeName) + .addModifiers(KModifier.EXTERNAL, KModifier.PRIVATE) + .returns((if (signature.isSuspend) signature.suspendResult else signature.result).encodedType.jvmOrNull?.name ?: UNIT) + + // Create the code block that invokes the jni function + val callParameters = mutableMapOf() + + // PARAMETERS + // Class members need to pass "this.knee" to the external function + /* signature.extraParameters.forEach { (param, type) -> + val name = param.asStringSafeForCodegen() + bridgeSpec.addParameter(name, type.kpType) + if (param == FunctionSignature.Extra.Receiver) { + callParameters[name] = ClassHandleName + } else { + callParameters[name] = name + } + } */ + signature.extraParameters.forEach { (param, codec) -> + val name = param.asStringSafeForCodegen(true) + with(codec) { + bridgeSpec.addParameter(name, encodedType.jvmOrNull!!.name) + callParameters[name] = codegenEncode( + codegenContext = codecContext, + local = if (param == DownwardFunctionSignature.Extra.ReceiverInstance) "this" else name + ) + } + } + // Regular parameters, to be propagated after proper mapping + signature.regularParameters.forEach { (param, codec) -> + val name = param.asStringSafeForCodegen(true) + // println("codegenLocalToJni param: ${param.name} safe: $name") + with(codec) { + bridgeSpec.addParameter(name, encodedType.jvmOrNull!!.name) + callParameters[name] = codegenEncode(codecContext, name) + } + } + + // CALL + // Need to flatten the call parameters in a single invocation line + addNamed( + format = "$prefix`$bridgeName`(${bridgeSpec.parameters.joinToString { "%${it.name}:L" }})\n", + arguments = callParameters + ) + return bridgeSpec + } + + /** + * Process the result of [codegenInvoke] - before returning it to local, + * it might need conversion. + */ + fun CodeBlock.Builder.codegenReceive( + rawValue: String, + signature: DownwardFunctionSignature, + prefix: String, // e.g. "return " + codecContext: CodegenCodecContext, + suspendToken: Boolean = false + ) { + val returnType = if (suspendToken) signature.suspendResult else signature.result + val decodedValue = when { + !returnType.needsCodegenConversion -> rawValue + else -> with(returnType) { codegenDecode(codecContext, rawValue) } + } + addStatement("$prefix$decodedValue") + } +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/functions/DownwardFunctionsIr.kt b/knee-compiler-plugin/src/main/kotlin/functions/DownwardFunctionsIr.kt new file mode 100644 index 0000000..b48bf09 --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/functions/DownwardFunctionsIr.kt @@ -0,0 +1,68 @@ +package io.deepmedia.tools.knee.plugin.compiler.functions + +import io.deepmedia.tools.knee.plugin.compiler.codec.IrCodecContext +import io.deepmedia.tools.knee.plugin.compiler.functions.DownwardFunctionsIr.irInvoke +import org.jetbrains.kotlin.ir.builders.* +import org.jetbrains.kotlin.ir.declarations.IrFunction +import org.jetbrains.kotlin.ir.declarations.IrValueParameter +import org.jetbrains.kotlin.ir.expressions.IrExpression +import org.jetbrains.kotlin.ir.util.fqNameWhenAvailable + +/** + * IR companion of [DownwardFunctionsCodegen]. + */ +object DownwardFunctionsIr { + + /** + * Calls the original, local function from bridge, mapping all inputs. + * Returns the raw output, not mapped. + */ + fun IrStatementsBuilder<*>.irInvoke( + inputs: List, + local: IrFunction, + signature: DownwardFunctionSignature, + codecContext: IrCodecContext, + ): IrExpression { + val logPrefix = "FunctionsIr.irInvoke(${local.fqNameWhenAvailable})" + codecContext.logger.injectLog(this, "$logPrefix START") + + return irCall(local).apply { + val hasReceiver = signature.extraParameters.firstOrNull { it.first == DownwardFunctionSignature.Extra.ReceiverInstance } + hasReceiver?.let { (name, codec) -> + val param = inputs.first { it.name == name } + codecContext.logger.injectLog(this@irInvoke, "$logPrefix Decoding dispatch receiver $name with $codec") + dispatchReceiver = with(codec) { irDecode(codecContext, param) } + } + signature.regularParameters.forEachIndexed { index, (param, codec) -> + with(codec) { + // note: targetIndex != index because of copy parameters! + val inputIndex = index + signature.knPrefixParameters.size + signature.extraParameters.size + val targetIndex = local.valueParameters.indexOfFirst { it.name == param } + codecContext.logger.injectLog(this@irInvoke, "$logPrefix Decoding parameter $param with $codec") + putValueArgument(targetIndex, irDecode(codecContext, inputs[inputIndex])) + } + } + /* signature.knCopyParameters.forEach { (param, indexToBeCopied) -> + val targetIndex = local.valueParameters.indexOfFirst { it.name == param } + putValueArgument(targetIndex, irGet(inputs[indexToBeCopied])) + } */ + } + } + + /** + * Process the result of [irInvoke] - before returning it to bridge, + * it might need conversion. + */ + fun IrStatementsBuilder<*>.irReceive( + rawValue: IrExpression, + signature: DownwardFunctionSignature, + codecContext: IrCodecContext, + suspendToken: Boolean = false + ): IrExpression { + val returnType = if (suspendToken) signature.suspendResult else signature.result + if (!returnType.needsIrConversion) return rawValue + return with(returnType) { + irEncode(codecContext, irTemporary(rawValue, "result")) + } + } +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/functions/UpwardFunctionSignature.kt b/knee-compiler-plugin/src/main/kotlin/functions/UpwardFunctionSignature.kt new file mode 100644 index 0000000..a2e6edf --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/functions/UpwardFunctionSignature.kt @@ -0,0 +1,145 @@ +package io.deepmedia.tools.knee.plugin.compiler.functions + +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import com.squareup.kotlinpoet.UNIT +import io.deepmedia.tools.knee.plugin.compiler.codec.Codec +import io.deepmedia.tools.knee.plugin.compiler.codec.GenericCodec +import io.deepmedia.tools.knee.plugin.compiler.codec.IdentityCodec +import io.deepmedia.tools.knee.plugin.compiler.codegen.CodegenType +import io.deepmedia.tools.knee.plugin.compiler.context.KneeMapper +import io.deepmedia.tools.knee.plugin.compiler.symbols.KneeSymbols +import io.deepmedia.tools.knee.plugin.compiler.features.KneeUpwardFunction +import io.deepmedia.tools.knee.plugin.compiler.import.concrete +import io.deepmedia.tools.knee.plugin.compiler.jni.JniSignature +import io.deepmedia.tools.knee.plugin.compiler.jni.JniType +import io.deepmedia.tools.knee.plugin.compiler.utils.* +import io.deepmedia.tools.knee.plugin.compiler.symbols.RuntimeIds.KneeSuspendInvocation +import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction +import org.jetbrains.kotlin.ir.declarations.IrValueParameter +import org.jetbrains.kotlin.ir.symbols.IrClassSymbol +import org.jetbrains.kotlin.ir.symbols.IrScriptSymbol +import org.jetbrains.kotlin.ir.symbols.IrTypeParameterSymbol +import org.jetbrains.kotlin.ir.util.* +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.util.capitalizeDecapitalize.capitalizeAsciiOnly + + +class UpwardFunctionSignature( + source: IrSimpleFunction, + kind: KneeUpwardFunction.Kind, + symbols: KneeSymbols, + mapper: KneeMapper +) { + + // Jvm only. + object Extra { + val SuspendInvoker = DownwardFunctionSignature.Extra.SuspendInvoker + val Receiver = DownwardFunctionSignature.Extra.ReceiverInstance + } + + val isSuspend = source.isSuspend + + // Reverse suspend functions need to pass the return type from JVM to KN through a function + // and there's no easy way to generate the exact signature. So we wrap it into java.lang.Object + // This can be achieved by wrapping the codec with a GenericCodec + val result: Codec = run { + val codec = mapper.get(source.returnType.simple("UpwardSignature.result").concrete(kind.importInfo), source) + when { + !isSuspend -> codec + else -> GenericCodec(symbols, codec) + } + } + + // Suspend function have a direct return type of KneeSuspendInvocation on JVM, jobject on KN + // This type should not be encoded or decoded so we wrap in a IdentityCodec + // Note that Raw might be unit if the function returned void + val suspendResult: Codec = IdentityCodec(JniType.Object(symbols, CodegenType.from( + poetType = ClassName.bestGuess(KneeSuspendInvocation.asString()).parameterizedBy( + typeArguments = arrayOf(result.encodedType.jvmOrNull?.name ?: UNIT) + ) + ))) + + // Note: this is the KneeInterface mapper, which technically encodes to Any and passes either a jobject or a long. + // But reverse functions are only used for the K/N Impl classes, which point to a JVM implementation. + // In this case the mapper passes the object as is, it's never a long. + // val dispatchReceiver: Codec = mapper.get(localIrType = source.parentAsClass.thisReceiver!!.type) + + + val extraParameters: List> = buildList { + // Receiver: passing it as jobject, as is. => need identity codec + // Suspend invoker: passing it as long + add(Extra.Receiver to IdentityCodec(JniType.Object( + symbols = symbols, + jvm = CodegenType.from(source.parentAsClass.thisReceiver!!.type.simple("UpwardSignature.extraParams").concrete(kind.importInfo)) + ))) + if (isSuspend) { + add(Extra.SuspendInvoker to mapper.get(symbols.builtIns.longType)) + } + } + + val regularParameters: List> = source.valueParameters.map { + it.name to mapper.get(it.type.simple("UpwardSignature.regularParams").concrete(kind.importInfo), it) + } + + val jniInfo = JniInfo(source) + + inner class JniInfo internal constructor( + private val source: IrSimpleFunction, + ) { + @Suppress("DefaultLocale") + fun name(includeAncestors: Boolean): Name { + val suffix = source.valueParameters.makeFunctionNameDisambiguationSuffix() + + fun mapper(name: String): String = "_\$" + when { + source.isGetter -> "get${source.correspondingPropertySymbol!!.owner.name.asString().capitalizeAsciiOnly()}" + source.isSetter -> "set${source.correspondingPropertySymbol!!.owner.name.asString().capitalizeAsciiOnly()}" + else -> listOfNotNull(name, suffix).joinToString(separator = "_") + } + // Since this is used in codegen, it can't be a special name + return when { + includeAncestors -> source.codegenUniqueName(false, ::mapper) + else -> source.codegenName.map(false, ::mapper) + } + } + + val signature: String = run { + val returnType: JniType = (if (isSuspend) suspendResult else result).encodedType + val prefixTypes: List = extraParameters.map { (_, codec) -> codec.encodedType } + val actualTypes: List = regularParameters.map { (_, codec) -> codec.encodedType } + JniSignature.get( + returnType = returnType, + argumentTypes = prefixTypes + actualTypes + ) + } + } +} + +/** + * Used when creating synthetic function names to disambiguate. + * Needed because different types might use the same internal representation (e.g. two enums) + * so there would be a clash between, say, getFoo(bar: Bar) and getFoo(baz: Baz) if Bar and Baz are enums + * + * Here we disambiguate by using the innermost type name, without looking at the package to avoid huge function names. + * But that may be needed in the long run. + * Note that old impl using IrType.hashCode and/or IrType.disambiguationHash was causing problems. + */ +internal fun List.makeFunctionNameDisambiguationSuffix(): String? { + if (this.isEmpty()) return null + return map { + val simpleType = it.type.simple("SignatureDisambiguation") + val someName = when (val classifier = simpleType.classifier) { + // relativeClassName is better in case of nested classes, e.g. Audio.Profile vs. Video.Profile + is IrClassSymbol -> when (val classId = classifier.owner.classId) { + null -> classifier.owner.name + else -> { + val segments = classId.relativeClassName.pathSegments() + Name.identifier(segments.joinToString("") { it.asStringSafeForCodegen(false) }) + } + } + is IrTypeParameterSymbol -> classifier.owner.name + is IrScriptSymbol -> classifier.owner.name + } + someName.asStringSafeForCodegen(false) + }.joinToString(separator = "") +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/functions/UpwardFunctionsCodegen.kt b/knee-compiler-plugin/src/main/kotlin/functions/UpwardFunctionsCodegen.kt new file mode 100644 index 0000000..672da7a --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/functions/UpwardFunctionsCodegen.kt @@ -0,0 +1,72 @@ +package io.deepmedia.tools.knee.plugin.compiler.functions + +import com.squareup.kotlinpoet.CodeBlock +import com.squareup.kotlinpoet.UNIT +import io.deepmedia.tools.knee.plugin.compiler.codec.CodegenCodecContext +import io.deepmedia.tools.knee.plugin.compiler.utils.asStringSafeForCodegen +import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction +import org.jetbrains.kotlin.ir.util.isGetter +import org.jetbrains.kotlin.ir.util.isSetter + +/** + * Codegen companion of [UpwardFunctionsIr]. + */ +object UpwardFunctionsCodegen { + + /** + * Calls the local function from the jni wrapper function, mapping all inputs. + * Returns the raw output, unprocessed. Use [codegenReceive] to process it. + */ + fun CodeBlock.Builder.codegenInvoke( + signature: UpwardFunctionSignature, + prefix: String, // e.g. "return " + codecContext: CodegenCodecContext, + ) { + // addStatement("println(\"DEBUG: ${codecContext.functionSymbol.owner.name} (reverse) invoked\")") + // Create the code block that invokes the jni function + val parameters = LinkedHashMap() // order matters + + // PARAMETERS + // Regular parameters, to be propagated after proper mapping + signature.regularParameters.forEach { (param, codec) -> + val name = param.asStringSafeForCodegen(true) + with(codec) { parameters[name] = codegenDecode(codecContext, name) } + } + + // CALL + val receiver = UpwardFunctionSignature.Extra.Receiver + val func = codecContext.functionSymbol!!.owner as IrSimpleFunction + when { + func.isSetter -> { + check(parameters.size == 1) { "Setter should have only 1 parameter, found: $parameters" } + addStatement("$receiver.${func.correspondingPropertySymbol!!.owner.name} = ${parameters.values.single()}") + addStatement("${prefix}%T", UNIT) + } + func.isGetter -> { + check(parameters.size == 0) { "Getter should have no parameters, found: $parameters" } + addStatement("$prefix$receiver.${func.correspondingPropertySymbol!!.owner.name}") + } + else -> { + val parametersFormat = parameters.keys.joinToString { "%${it}:L" } + addNamed("$prefix$receiver.${func.name}($parametersFormat)\n", parameters) + } + } + } + + fun CodeBlock.Builder.codegenReceive( + rawValue: String, + signature: UpwardFunctionSignature, + prefix: String, // e.g. "return " + codecContext: CodegenCodecContext, + suspendToken: Boolean = false, + ) { + val returnType = if (suspendToken) signature.suspendResult else signature.result + val decodedValue = when { + !returnType.needsCodegenConversion -> rawValue + else -> with(returnType) { codegenEncode(codecContext, rawValue) } + } + addStatement("$prefix$decodedValue") + } + + +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/functions/UpwardFunctionsIr.kt b/knee-compiler-plugin/src/main/kotlin/functions/UpwardFunctionsIr.kt new file mode 100644 index 0000000..d516be9 --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/functions/UpwardFunctionsIr.kt @@ -0,0 +1,105 @@ +package io.deepmedia.tools.knee.plugin.compiler.functions + +import io.deepmedia.tools.knee.plugin.compiler.symbols.KneeSymbols +import io.deepmedia.tools.knee.plugin.compiler.jni.JniType +import io.deepmedia.tools.knee.plugin.compiler.codec.IrCodecContext +import io.deepmedia.tools.knee.plugin.compiler.symbols.RuntimeIds.callStaticMethod +import org.jetbrains.kotlin.ir.builders.* +import org.jetbrains.kotlin.ir.declarations.IrValueParameter +import org.jetbrains.kotlin.ir.declarations.IrVariable +import org.jetbrains.kotlin.ir.expressions.IrExpression +import org.jetbrains.kotlin.ir.types.makeNullable +import org.jetbrains.kotlin.ir.util.fqNameWhenAvailable + +/** + * IR companion of [UpwardFunctionsCodegen]. + */ +object UpwardFunctionsIr { + + /** + * Calls the JVM function from IR using Jni utilities, mapping all inputs. + * Returns the raw output, not mapped. + */ + fun IrStatementsBuilder<*>.irInvoke( + symbols: KneeSymbols, + inputs: List, + signature: UpwardFunctionSignature, + codecContext: IrCodecContext, + jreceiver: IrVariable, + jmethodOwner: IrVariable, + jmethod: IrVariable, + returnJniType: JniType, + suspendInvoker: IrValueParameter? = null + ): IrExpression { + val logPrefix = "ReverseFunctionsIr.irInvoke(${codecContext.functionSymbol!!.owner.fqNameWhenAvailable})" + + // Take care of prefixes + codecContext.logger.injectLog(this, "$logPrefix START") + val prefixInputs = signature.extraParameters.map { (param, codec) -> + codecContext.logger.injectLog(this, "$logPrefix ENCODING prefix $param with $codec") + with(codec) { + irEncode(codecContext, local = when (param) { + UpwardFunctionSignature.Extra.Receiver -> jreceiver + UpwardFunctionSignature.Extra.SuspendInvoker -> suspendInvoker!! + else -> error("Unexpected prefix parameter: $param") + }) + } + } + + // Encode all inputs + val mappedInputs = signature.regularParameters.map { (param, codec) -> + codecContext.logger.injectLog(this, "$logPrefix ENCODING param $param with $codec") + with(codec) { irEncode(codecContext, inputs.first { it.name == param }) } + } + + val function = callStaticMethod(returnJniType.nameOfCallMethodFunction) + codecContext.logger.injectLog(this, "$logPrefix INVOKING $function") + + // By design we pass the receiver as argument instead of JNI receiver, because otherwise + // we'd have to add the JNI wrapper function inside the interface. We use companion object + JvmStatic instead. + return irCall( + symbols.functions(function).single() + ).apply { + extensionReceiver = irGet(codecContext.environment) + putValueArgument(0, irGet(jmethodOwner)) + putValueArgument(1, irGet(jmethod)) + putValueArgument(2, irVararg( + elementType = symbols.builtIns.anyType.makeNullable(), + values = prefixInputs + mappedInputs + )) + } + } + + private val JniType.nameOfCallMethodFunction: String get() { + return when (this) { + is JniType.Void -> "Void" + is JniType.Object, is JniType.Array -> "Object" + is JniType.Int -> "Int" + is JniType.BooleanAsUByte -> "Boolean" + is JniType.Float -> "Float" + is JniType.Double -> "Double" + is JniType.Byte -> "Byte" + is JniType.Long -> "Long" + } + } + + + /** + * Process the result of [irInvoke] - before returning it might need conversion. + */ + fun IrStatementsBuilder<*>.irReceive( + rawValue: IrExpression, + signature: UpwardFunctionSignature, + codecContext: IrCodecContext, + suspendToken: Boolean = false + ): IrExpression { + val logPrefix = "ReverseFunctionsIr.irReceive(${codecContext.functionSymbol!!.owner.fqNameWhenAvailable})" + + val returnType = if (suspendToken) signature.suspendResult else signature.result + if (!returnType.needsIrConversion) return rawValue + return with(returnType) { + codecContext.logger.injectLog(this@irReceive, "$logPrefix DECODING return type with $returnType") + irDecode(codecContext, irTemporary(rawValue, "result")) + } + } +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/import/ImportInfo.kt b/knee-compiler-plugin/src/main/kotlin/import/ImportInfo.kt new file mode 100644 index 0000000..ffe7f79 --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/import/ImportInfo.kt @@ -0,0 +1,46 @@ +package io.deepmedia.tools.knee.plugin.compiler.import + +import com.squareup.kotlinpoet.TypeVariableName +import io.deepmedia.tools.knee.plugin.compiler.utils.asTypeName +import io.deepmedia.tools.knee.plugin.compiler.utils.simple +import org.jetbrains.kotlin.backend.common.lower.parents +import org.jetbrains.kotlin.ir.IrBuiltIns +import org.jetbrains.kotlin.ir.builders.declarations.buildClass +import org.jetbrains.kotlin.ir.declarations.* +import org.jetbrains.kotlin.ir.types.* +import org.jetbrains.kotlin.ir.util.* + +class ImportInfo( + val type: IrSimpleType, + private val declaration: IrDeclarationWithName, +) { + val id: String get() = declaration.name.asString() + + // this is writable! + val file: IrFile get() = declaration.file + + private val typeParameters = type.classOrNull!!.owner.typeParameters.map { it.symbol } + private val typeArguments = type.arguments + + val typeVariables = typeParameters.map { + // type = kotlin.ranges.ClosedRange + // declaration.name = closedFloatRange + // type parameter = T + // super type = kotlin.Comparable + TypeVariableName( + name = it.owner.name.asString(), + bounds = it.owner.superTypes.map { + it.simple("ImportInfo.typeParameters.map").asTypeName() + } + ) + } + + val substitutor = IrTypeSubstitutor( + typeParameters = typeParameters, + typeArguments = typeArguments, + allowEmptySubstitution = false + ) + + // Does the same that substitutor, to be used in function.copyValueParametersFrom... + val substitutionMap = typeParameters.zip(typeArguments.map { it.typeOrNull!! }).toMap() +} diff --git a/knee-compiler-plugin/src/main/kotlin/import/ImportUtils.kt b/knee-compiler-plugin/src/main/kotlin/import/ImportUtils.kt new file mode 100644 index 0000000..87700f5 --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/import/ImportUtils.kt @@ -0,0 +1,55 @@ +package io.deepmedia.tools.knee.plugin.compiler.import + +import io.deepmedia.tools.knee.plugin.compiler.context.KneeContext +import io.deepmedia.tools.knee.plugin.compiler.context.KneeOrigin +import io.deepmedia.tools.knee.plugin.compiler.utils.isPartOf +import org.jetbrains.kotlin.ir.builders.declarations.buildClass +import org.jetbrains.kotlin.ir.declarations.* +import org.jetbrains.kotlin.ir.types.* +import org.jetbrains.kotlin.ir.util.* + +fun IrSimpleType.concrete(importInfo: ImportInfo?): IrSimpleType = when (importInfo) { + null -> this + else -> importInfo.substitutor.substitute(this).let { + checkNotNull(it as? IrSimpleType) { "Substitute of $this is not a simple type" } + } +} + +/** + * Recreates the hierarchy of an imported declaration in the import location, up to the parent of this declaration. + * All generated classes are marked as [KneeOrigin.KNEE_IMPORT_PARENT]. + */ +fun IrDeclaration.writableParent(context: KneeContext, importInfo: ImportInfo?): IrDeclarationParent { + if (isPartOf(context.module)) return parent + requireNotNull(importInfo) { + "Declaration $this is external but no ImportInfo provided." + } + + var candidate: IrDeclarationContainer = importInfo.file + val parentClasses = parents.takeWhile { it !is IrPackageFragment }.toList().reversed().toMutableList() + + // We could use deepCopy, but then it's pretty complex to reconcile different trees if writableParent is + // called multiple times within the same tree. + while (parentClasses.isNotEmpty()) { + val next = parentClasses.removeFirst() + require(next is IrClass) { "Declaration parent is not an IrClass, not sure what to do: $next" } + var nextCopy = candidate.findDeclaration { it.name == next.name } + if (nextCopy != null) { + check(nextCopy.origin == KneeOrigin.KNEE_IMPORT_PARENT) { + "Origin mismatch! Element: ${nextCopy!!.fqNameWhenAvailable} has: ${nextCopy!!.origin}" + } + candidate = nextCopy + } else { + nextCopy = context.factory.buildClass { + modality = next.modality + origin = KneeOrigin.KNEE_IMPORT_PARENT + visibility = next.visibility + name = next.name + }.also { it.parent = candidate } + candidate.addChild(nextCopy) + candidate = nextCopy + } + } + + return candidate +} diff --git a/knee-compiler-plugin/src/main/kotlin/instances/InstancesCodegen.kt b/knee-compiler-plugin/src/main/kotlin/instances/InstancesCodegen.kt new file mode 100644 index 0000000..4b11320 --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/instances/InstancesCodegen.kt @@ -0,0 +1,70 @@ +package io.deepmedia.tools.knee.plugin.compiler.instances + +import com.squareup.kotlinpoet.* + + +object InstancesCodegen { + // Note: this is also hardcoded in kneeUnwrapInstance for exception handling (knee-runtime) + const val HandleField = "\$knee" + + /** + * [preserveSymbols]: whether it should be allowed to act on the [HandleField] constructor + * and/or field, for example through the JVM runtime functions `kneeWrapInstance` and `kneeUnwrapInstance` + * (which use reflection) or simply via JNI (which doesn't respect access control modifiers anyway). + * + * Since we don't want to make them public, we use internal + [PublishedApi]. + */ + fun TypeSpec.Builder.addHandleConstructorAndField( + preserveSymbols: Boolean, + ) { + primaryConstructor( + FunSpec.constructorBuilder() + .addModifiers(KModifier.INTERNAL) + .apply { + if (preserveSymbols) addAnnotation(PublishedApi::class) + } + .addParameter(HandleField, LONG) + .build() + ) + addProperty( + PropertySpec.builder(HandleField, LONG) + .addModifiers(KModifier.INTERNAL) + .addAnnotation(JvmField::class) + .apply { + if (preserveSymbols) addAnnotation(PublishedApi::class) + } + .initializer(HandleField) + .build()) + } + + fun TypeSpec.Builder.addObjectOverrides(verbose: Boolean) { + val pkg = "io.deepmedia.tools.knee.runtime.compiler" + val type = this.build().name!! + addFunction(FunSpec.builder("finalize") + .let { if (verbose) it.addKdoc("knee:instances") else it } + .addModifiers(KModifier.PROTECTED) + .returns(UNIT) + .addCode("%M(`$HandleField`)", MemberName(pkg, "kneeDisposeInstance")) + .build()) + addFunction(FunSpec.builder("toString") + .let { if (verbose) it.addKdoc("knee:instances") else it } + .addModifiers(KModifier.OVERRIDE) + .returns(STRING) + .addCode("return %M(`$HandleField`)", MemberName(pkg, "kneeDescribeInstance")) + .build()) + addFunction(FunSpec.builder("hashCode") + .let { if (verbose) it.addKdoc("knee:instances") else it } + .addModifiers(KModifier.OVERRIDE) + .returns(INT) + .addCode("return %M(`$HandleField`)", MemberName(pkg, "kneeHashInstance")) + .build()) + addFunction(FunSpec.builder("equals") + .let { if (verbose) it.addKdoc("knee:instances") else it } + .addModifiers(KModifier.OVERRIDE) + .addParameter("other", ANY.copy(nullable = true)) + .returns(BOOLEAN) + .addCode("return other is `$type` && %M(`$HandleField`, other.`$HandleField`)", MemberName(pkg, "kneeCompareInstance")) + .build()) + } + +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/instances/InterfaceNames.kt b/knee-compiler-plugin/src/main/kotlin/instances/InterfaceNames.kt new file mode 100644 index 0000000..f7dc9a8 --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/instances/InterfaceNames.kt @@ -0,0 +1,22 @@ +package io.deepmedia.tools.knee.plugin.compiler.instances + +import io.deepmedia.tools.knee.plugin.compiler.import.ImportInfo +import io.deepmedia.tools.knee.plugin.compiler.utils.map +import org.jetbrains.kotlin.name.Name + +/** + * Interface called 'Foo' becomes: + * - KneeFoo (if not imported) + * - KneeFoo$propertyName (if imported through property) + */ +object InterfaceNames { + // val interfacePrefixMapper: (String) -> String = { "Knee$it" } + + private fun interfaceNameMapper(importInfo: ImportInfo?): (String) -> String { + return { "Knee$it${importInfo?.let { info -> "$${info.id}" } ?: ""}" } + } + + fun Name.asInterfaceName(importInfo: ImportInfo?): Name = map(block = interfaceNameMapper(importInfo)) + + fun String.asInterfaceName(importInfo: ImportInfo?): String = interfaceNameMapper(importInfo).invoke(this) +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/jni/JniSignature.kt b/knee-compiler-plugin/src/main/kotlin/jni/JniSignature.kt new file mode 100644 index 0000000..5055492 --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/jni/JniSignature.kt @@ -0,0 +1,63 @@ +package io.deepmedia.tools.knee.plugin.compiler.jni + +object JniSignature { + + fun get(type: JniType): String = buildString { + appendJniType(type, isReturnType = true) + } + + fun get(returnType: JniType, argumentTypes: List): String = buildString { + append('(') + argumentTypes.forEach { + appendJniType(it, isReturnType = false) + } + append(')') + appendJniType(returnType, isReturnType = true) + } + + /** + * Table 3-2 Java VM Type Signatures + * Z = boolean + * B = byte + * C = char + * S = short + * I = int + * J = long + * F = float + * D = double + * L; = class + * [ = array type + * () = method + */ + private fun StringBuilder.appendJniType(type: JniType, isReturnType: Boolean) { + when (type) { + is JniType.Void -> { + require(isReturnType) { "JniType.Void is not allowed here." } + append('V') + } + + // PRIMITIVE TYPES + is JniType.BooleanAsUByte -> append('Z') + is JniType.Byte -> append('B') + // builtIns.charType -> append('C') + // builtIns.shortType -> append('S') + is JniType.Int -> append('I') + is JniType.Long -> append('J') + is JniType.Float -> append('F') + is JniType.Double -> append('D') + + // OBJECT TYPES + is JniType.Object -> { + append('L') + append(type.jvm.jvmClassName) // cares about dollar signs + append(';') + } + + // ARRAY TYPES + is JniType.Array -> { + append("[") + appendJniType(type.element, isReturnType) + } + } + } +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/jni/JniType.kt b/knee-compiler-plugin/src/main/kotlin/jni/JniType.kt new file mode 100644 index 0000000..7ca8cfd --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/jni/JniType.kt @@ -0,0 +1,128 @@ +package io.deepmedia.tools.knee.plugin.compiler.jni + +import com.squareup.kotlinpoet.* +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import io.deepmedia.tools.knee.plugin.compiler.codegen.CodegenType +import io.deepmedia.tools.knee.plugin.compiler.symbols.KneeSymbols +import io.deepmedia.tools.knee.plugin.compiler.symbols.KotlinIds +import io.deepmedia.tools.knee.plugin.compiler.symbols.PlatformIds +import io.deepmedia.tools.knee.plugin.compiler.utils.simpleName +import kotlinx.serialization.Contextual +import kotlinx.serialization.Serializable +import org.jetbrains.kotlin.ir.types.* + +/** + * All possible types that can pass the JNI interface. + * https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/types.html + * Note that conversion between [knOrNull] and [jvmOrNull] is done automatically by the JNI. + */ +@Serializable +sealed interface JniType { + + val knOrNull: IrSimpleType? get() = when (this) { + is Real -> kn + else -> null + } + + val jvmOrNull: CodegenType? get() = when (this) { + is Real -> jvm + else -> null + } + + /** A very special type, allowed only in return types of function, not to be used elsewhere */ + @Serializable + object Void : JniType + + @Serializable + sealed interface Real : JniType { + val kn: IrSimpleType + val jvm: CodegenType + } + + @Serializable + sealed interface Primitive : Real { + // local simple names: for most primitives they are the same, but for some they aren't. + // E.g. for JniType.Boolean, jvm = "Boolean" and kn = "UByte" + val jvmSimpleName get() = jvm.name.simpleName + val knSimpleName get() = kn.let { CodegenType.from(it) }.name.simpleName + fun array(symbols: KneeSymbols): Array + } + + @Serializable + class Int private constructor(@Contextual override val kn: IrSimpleType) : Primitive { + constructor(symbols: KneeSymbols) : this(kn = symbols.builtIns.intType as IrSimpleType) + override val jvm get() = CodegenType.from(INT) + override fun array(symbols: KneeSymbols): Array { + return Array(symbols, CodegenType.from(INT_ARRAY), Int(symbols)) + } + } + + @Serializable + class Float private constructor(@Contextual override val kn: IrSimpleType) : Primitive { + constructor(symbols: KneeSymbols) : this(kn = symbols.builtIns.floatType as IrSimpleType) + override val jvm get() = CodegenType.from(FLOAT) + override fun array(symbols: KneeSymbols): Array { + return Array(symbols, CodegenType.from(FLOAT_ARRAY), Float(symbols)) + } + } + + @Serializable + class Double private constructor(@Contextual override val kn: IrSimpleType) : Primitive { + constructor(symbols: KneeSymbols) : this(kn = symbols.builtIns.doubleType as IrSimpleType) + override val jvm get() = CodegenType.from(DOUBLE) + override fun array(symbols: KneeSymbols): Array { + return Array(symbols, CodegenType.from(DOUBLE_ARRAY), Double(symbols)) + } + } + + @Serializable + class Long private constructor(@Contextual override val kn: IrSimpleType) : Primitive { + constructor(symbols: KneeSymbols) : this(kn = symbols.builtIns.longType as IrSimpleType) + override val jvm get() = CodegenType.from(LONG) + override fun array(symbols: KneeSymbols): Array { + return Array(symbols, CodegenType.from(LONG_ARRAY), Long(symbols)) + } + } + + @Serializable + class Byte private constructor(@Contextual override val kn: IrSimpleType) : Primitive { + constructor(symbols: KneeSymbols) : this(kn = symbols.builtIns.byteType as IrSimpleType) + override val jvm get() = CodegenType.from(BYTE) + override fun array(symbols: KneeSymbols): Array { + return Array(symbols, CodegenType.from(BYTE_ARRAY), Byte(symbols)) + } + } + + // The name makes it immediately clear that the types at the two ends are different + @Serializable + class BooleanAsUByte private constructor(@Contextual override val kn: IrSimpleType) : Primitive { + constructor(symbols: KneeSymbols) : this(kn = symbols.klass(KotlinIds.UByte).defaultType as IrSimpleType) + override val jvm get() = CodegenType.from(BOOLEAN) + override fun array(symbols: KneeSymbols): Array { + return Array(symbols, CodegenType.from(BOOLEAN_ARRAY), BooleanAsUByte(symbols)) + } + } + + @Serializable + class Object private constructor(@Contextual override val kn: IrSimpleType, override val jvm: CodegenType) : Real { + constructor(symbols: KneeSymbols, jvm: CodegenType) : this( + kn = symbols.typeAliasUnwrapped(PlatformIds.jobject) as IrSimpleType, + jvm = jvm + ) + fun array(symbols: KneeSymbols): Array { + return Array(symbols, CodegenType.from(ARRAY.parameterizedBy(jvm.name)), Object(symbols, jvm)) + } + } + + // Can be array of primitive or array of object + @Serializable + class Array private constructor( + @Contextual override val kn: IrSimpleType, + override val jvm: CodegenType, + val element: Real + ) : Real { + constructor(symbols: KneeSymbols, jvm: CodegenType, element: Real) : this( + symbols.typeAliasUnwrapped(PlatformIds.jobjectArray) as IrSimpleType, jvm, element + ) + } +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/metadata/ModuleMetadata.kt b/knee-compiler-plugin/src/main/kotlin/metadata/ModuleMetadata.kt new file mode 100644 index 0000000..faaa7ee --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/metadata/ModuleMetadata.kt @@ -0,0 +1,75 @@ +package io.deepmedia.tools.knee.plugin.compiler.metadata + +import io.deepmedia.tools.knee.plugin.compiler.context.KneeContext +import io.deepmedia.tools.knee.plugin.compiler.export.v2.ExportedTypeInfo +import io.deepmedia.tools.knee.plugin.compiler.symbols.AnnotationIds +import kotlinx.serialization.Contextual +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder +import org.jetbrains.kotlin.ir.builders.irCallConstructor +import org.jetbrains.kotlin.ir.builders.irString +import org.jetbrains.kotlin.ir.declarations.IrClass +import org.jetbrains.kotlin.ir.expressions.IrConst +import org.jetbrains.kotlin.ir.util.constructors +import org.jetbrains.kotlin.ir.util.dumpKotlinLike +import org.jetbrains.kotlin.ir.util.getAnnotation + + +@Serializable +data class ModuleMetadata private constructor( + @Contextual private val dependencyModules_: List, + val exportedTypes: List, +) { + + constructor( + exportedTypes: List, + dependencyModules: List, + nothing: Unit = Unit // fixes "same signature" with primary constructor + ) : this( + exportedTypes = exportedTypes, + dependencyModules_ = dependencyModules.map { IrClass_(it) } + ) + + val dependencyModules: List get() = dependencyModules_.map { it.irClass } + + // Pointless wrapper to fix a K2 serialization error + @Serializable + private data class IrClass_(@Contextual val irClass: IrClass) + + companion object { + fun read(module: IrClass, json: Json): ModuleMetadata? { + @Suppress("UNCHECKED_CAST") + val encoded = module.getAnnotation(AnnotationIds.KneeMetadata.asSingleFqName()) + ?.getValueArgument(0) + ?.let { it as? IrConst } + ?.value ?: return null + return json.decodeFromString(encoded) + } + } + + fun write(module: IrClass, context: KneeContext) { + val existingMetadataAnnotation = module.getAnnotation(AnnotationIds.KneeMetadata.asSingleFqName()) + check(existingMetadataAnnotation == null) { + "Module $module should not be annotated by @KneeMetadata(${existingMetadataAnnotation?.getValueArgument(0)?.dumpKotlinLike()})" + } + + val metadataString = try { + context.json.encodeToString(this) + } catch (e: Throwable) { + val canEncodeClassOnly = runCatching { context.json.encodeToString(IrClass_(module)) } + throw RuntimeException("Failed to encode ModuleMetadata (canEncodeClassOnly? ${canEncodeClassOnly})", e) + } + + context.plugin.metadataDeclarationRegistrar.addMetadataVisibleAnnotationsToElement( + declaration = module, + annotations = listOf(with(DeclarationIrBuilder(context.plugin, module.symbol)) { + val metadataConstructor = context.symbols.klass(AnnotationIds.KneeMetadata).constructors.single() + irCallConstructor(metadataConstructor, emptyList()).apply { + putValueArgument(0, irString(metadataString)) + } + }) + ) + } +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/serialization/Classes.kt b/knee-compiler-plugin/src/main/kotlin/serialization/Classes.kt new file mode 100644 index 0000000..10d2ce6 --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/serialization/Classes.kt @@ -0,0 +1,47 @@ +package io.deepmedia.tools.knee.plugin.compiler.serialization + +import io.deepmedia.tools.knee.plugin.compiler.symbols.KneeSymbols +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import org.jetbrains.kotlin.ir.declarations.IrClass +import org.jetbrains.kotlin.ir.util.classIdOrFail +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.FqName + +fun IrClassListSerializer(symbols: KneeSymbols) = ListSerializer(IrClassSerializer(symbols)) + +class IrClassSerializer(private val symbols: KneeSymbols) : KSerializer { + override val descriptor: SerialDescriptor get() = ClassIdSerializer.descriptor + override fun serialize(encoder: Encoder, value: IrClass) { + encoder.encodeSerializableValue(ClassIdSerializer, value.classIdOrFail) + } + override fun deserialize(decoder: Decoder): IrClass { + return symbols.klass(decoder.decodeSerializableValue(ClassIdSerializer)).owner + } +} + +object ClassIdSerializer : KSerializer { + override val descriptor get() = ClassIdSurrogate.serializer().descriptor + override fun serialize(encoder: Encoder, value: ClassId) { + encoder.encodeSerializableValue(ClassIdSurrogate.serializer(), ClassIdSurrogate(value)) + } + override fun deserialize(decoder: Decoder): ClassId { + return decoder.decodeSerializableValue(ClassIdSurrogate.serializer()).classId + } +} + +@Serializable +private data class ClassIdSurrogate( + @Serializable(with = FqNameSerializer::class) private val packageFqName: FqName, + @Serializable(with = FqNameSerializer::class) private val relativeClassName: FqName, + private val isLocal: Boolean +) { + constructor(classId: ClassId) : this(classId.packageFqName, classId.relativeClassName, classId.isLocal) + // constructor(klass: IrClass) : this(klass.classIdOrFail) + val classId get() = ClassId(packageFqName, relativeClassName, isLocal) + // fun klass(symbols: KneeSymbols): IrClass = symbols.klass(classId).owner +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/serialization/Names.kt b/knee-compiler-plugin/src/main/kotlin/serialization/Names.kt new file mode 100644 index 0000000..e301725 --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/serialization/Names.kt @@ -0,0 +1,73 @@ +package io.deepmedia.tools.knee.plugin.compiler.serialization + +import com.squareup.kotlinpoet.* +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.name.Name + + +object TypeNameSerializer : KSerializer { + override val descriptor get() = TypeNameSurrogate.serializer().descriptor + override fun serialize(encoder: Encoder, value: TypeName) { + encoder.encodeSerializableValue(TypeNameSurrogate.serializer(), TypeNameSurrogate(value)) + } + override fun deserialize(decoder: Decoder): TypeName { + return decoder.decodeSerializableValue(TypeNameSurrogate.serializer()).typeName + } +} + +@Serializable +private data class TypeNameSurrogate( + val packageName: String, + val simpleNames: List, + val typeArguments: List? +) { + val typeName: TypeName get() = when (typeArguments) { + null -> ClassName(packageName, simpleNames) + else -> ClassName(packageName, simpleNames).parameterizedBy(typeArguments.map { it.typeName }) + } + + companion object { + operator fun invoke(typeName: TypeName): TypeNameSurrogate = when (typeName) { + is ClassName -> TypeNameSurrogate(typeName.packageName, typeName.simpleNames, null) + is ParameterizedTypeName -> TypeNameSurrogate(typeName.rawType.packageName, typeName.rawType.simpleNames, typeName.typeArguments.map { TypeNameSurrogate(it) }) + is Dynamic, is LambdaTypeName, is TypeVariableName, is WildcardTypeName -> error("Unable to serialize this TypeName: $typeName") + } + } +} + +object NameSerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor( + "org.jetbrains.kotlin.name.Name", + PrimitiveKind.STRING + ) + + override fun deserialize(decoder: Decoder): Name { + return Name.guessByFirstCharacter(decoder.decodeString()) + } + + override fun serialize(encoder: Encoder, value: Name) { + encoder.encodeString(value.asString()) + } +} + +object FqNameSerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor( + "org.jetbrains.kotlin.name.FqName", + PrimitiveKind.STRING + ) + + override fun deserialize(decoder: Decoder): FqName { + return FqName(decoder.decodeString()) + } + + override fun serialize(encoder: Encoder, value: FqName) { + encoder.encodeString(value.asString()) + } +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/serialization/Types.kt b/knee-compiler-plugin/src/main/kotlin/serialization/Types.kt new file mode 100644 index 0000000..4a8e5de --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/serialization/Types.kt @@ -0,0 +1,47 @@ +package io.deepmedia.tools.knee.plugin.compiler.serialization + +import io.deepmedia.tools.knee.plugin.compiler.symbols.KneeSymbols +import kotlinx.serialization.Contextual +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import org.jetbrains.kotlin.ir.declarations.IrClass +import org.jetbrains.kotlin.ir.types.* +import org.jetbrains.kotlin.types.Variance + +class IrSimpleTypeSerializer(private val symbols: KneeSymbols) : KSerializer { + override val descriptor get() = IrSimpleTypeSurrogate.serializer().descriptor + override fun serialize(encoder: Encoder, value: IrSimpleType) { + encoder.encodeSerializableValue(IrSimpleTypeSurrogate.serializer(), IrSimpleTypeSurrogate(value)) + } + override fun deserialize(decoder: Decoder): IrSimpleType { + return decoder.decodeSerializableValue(IrSimpleTypeSurrogate.serializer()).simpleType(symbols) + } +} + +@Serializable +private data class IrSimpleTypeSurrogate( + private val nullable: Boolean, + private val typeArguments: List, + @Contextual private val classRef: IrClass +) { + constructor(type: IrSimpleType) : this( + nullable = type.isNullable(), + classRef = type.classOrFail.owner, + typeArguments = type.arguments.map { + check(it is IrTypeProjection) { "Type arguments should be IrTypeProjection, was: $it" } + check(it.variance == Variance.INVARIANT) { "Type arguments variance should be INVARIANT, was: ${it.variance}" } + val simpleType = checkNotNull(it.type as IrSimpleType) { "Type arguments should be IrSimpleType, was: ${it.type}" } + IrSimpleTypeSurrogate(simpleType) + // val klass = checkNotNull(it.type.classOrNull) { "Type arguments should be classes, was: ${it.type.dumpKotlinLike()}" } + // ModuleMetadataClass(klass.owner) + } + ) + fun simpleType(symbols: KneeSymbols): IrSimpleType { + // val args = typeArguments.map { it.klass(symbols).defaultType }.toTypedArray() + val args = typeArguments.map { it.simpleType(symbols) }.toTypedArray() + val res = classRef.typeWith(*args) + return res.withNullability(nullable) + } +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/services/KneeCommandLineProcessor.kt b/knee-compiler-plugin/src/main/kotlin/services/KneeCommandLineProcessor.kt new file mode 100644 index 0000000..d7a60fe --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/services/KneeCommandLineProcessor.kt @@ -0,0 +1,44 @@ +package io.deepmedia.tools.knee.plugin.compiler.services + +import org.jetbrains.kotlin.compiler.plugin.AbstractCliOption +import org.jetbrains.kotlin.compiler.plugin.CliOption +import org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor +import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi +import org.jetbrains.kotlin.config.CompilerConfiguration +import org.jetbrains.kotlin.config.CompilerConfigurationKey + +// https://github.com/ZacSweers/redacted-compiler-plugin/blob/main/redacted-compiler-plugin/src/main/kotlin/dev/zacsweers/redacted/compiler/RedactedCommandLineProcessor.kt +@OptIn(ExperimentalCompilerApi::class) +class KneeCommandLineProcessor : CommandLineProcessor { + companion object { + val KneeEnabled = CompilerConfigurationKey("enabled") + val KneeOutputDir = CompilerConfigurationKey("outputDir") + + val KneeVerboseLogs = CompilerConfigurationKey("verboseLogs") + val KneeVerboseRuntime = CompilerConfigurationKey("verboseRuntime") + val KneeVerboseSources = CompilerConfigurationKey("verboseSources") + // val KneeLegacyIo = CompilerConfigurationKey("legacyImportExport") + } + + override val pluginId: String = "knee-compiler-plugin" + + override val pluginOptions: Collection = listOf( + CliOption("enabled", "","Whether knee processing is enabled.", required = true), + CliOption("verboseLogs", "","Enable or disable plugin logs.", required = false), + CliOption("verboseRuntime", "","Enable or disable runtime logs.", required = false), + CliOption("verboseSources", "","Enable or disable JVM sources comments.", required = false), + CliOption("outputDir", "","Absolute path to the generated source code directory.", required = false), + // CliOption("legacyImportExport", "","Whether to use the legacy (K1 only) export/import logic.", required = false), + ) + + override fun processOption(option: AbstractCliOption, value: String, configuration: CompilerConfiguration) { + when (option.optionName) { + "enabled" -> configuration.put(KneeEnabled, value.toBoolean()) + "verboseLogs" -> configuration.put(KneeVerboseLogs, value.toBoolean()) + "verboseRuntime" -> configuration.put(KneeVerboseRuntime, value.toBoolean()) + "verboseSources" -> configuration.put(KneeVerboseSources, value.toBoolean()) + "outputDir" -> configuration.put(KneeOutputDir, value) + // "legacyImportExport" -> configuration.put(KneeLegacyIo, value.toBoolean()) + } + } +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/services/KneeComponentRegistrar.kt b/knee-compiler-plugin/src/main/kotlin/services/KneeComponentRegistrar.kt new file mode 100644 index 0000000..abd690b --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/services/KneeComponentRegistrar.kt @@ -0,0 +1,28 @@ +package io.deepmedia.tools.knee.plugin.compiler.services + +import io.deepmedia.tools.knee.plugin.compiler.KneeIrGeneration +import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension +import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys +import org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar +import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi +import org.jetbrains.kotlin.config.CompilerConfiguration +import java.io.File + +@OptIn(ExperimentalCompilerApi::class) +class KneeComponentRegistrar : CompilerPluginRegistrar() { + override fun ExtensionStorage.registerExtensions(configuration: CompilerConfiguration) { + if (configuration[KneeCommandLineProcessor.KneeEnabled] == false) return + val logs = configuration[CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY]!! + val verboseLogs = configuration[KneeCommandLineProcessor.KneeVerboseLogs] ?: false + val verboseRuntime = configuration[KneeCommandLineProcessor.KneeVerboseRuntime] ?: false + val verboseCodegen = configuration[KneeCommandLineProcessor.KneeVerboseSources] ?: false + val outputDir = File(configuration[KneeCommandLineProcessor.KneeOutputDir]!!) + IrGenerationExtension.registerExtension(KneeIrGeneration(logs, verboseLogs, verboseRuntime, verboseCodegen, outputDir, true)) + // if (legacyIo) { + // SyntheticResolveExtension.registerExtension(KneeSyntheticResolve()) + // } + } + + override val supportsK2: Boolean + get() = true +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/symbols/KneeSymbols.kt b/knee-compiler-plugin/src/main/kotlin/symbols/KneeSymbols.kt new file mode 100644 index 0000000..963add1 --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/symbols/KneeSymbols.kt @@ -0,0 +1,37 @@ +package io.deepmedia.tools.knee.plugin.compiler.symbols + +import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext +import org.jetbrains.kotlin.ir.IrBuiltIns +import org.jetbrains.kotlin.ir.symbols.IrClassSymbol +import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol +import org.jetbrains.kotlin.ir.symbols.IrTypeAliasSymbol +import org.jetbrains.kotlin.ir.types.IrType +import org.jetbrains.kotlin.name.CallableId +import org.jetbrains.kotlin.name.ClassId + + +class KneeSymbols(private val plugin: IrPluginContext) { + + val builtIns: IrBuiltIns get() = plugin.irBuiltIns + + private val classes2 = mutableMapOf() + private val typeAliases2 = mutableMapOf() + private val functions2 = mutableMapOf>() + + fun klass(classId: ClassId): IrClassSymbol = classes2.getOrPut(classId) { + requireNotNull(plugin.referenceClass(classId)) { "Could not find classId $classId" } + } + + fun functions(name: CallableId) = functions2.getOrPut(name) { + plugin.referenceFunctions(name).also { + require(it.isNotEmpty()) { "Could not find callableId $name" } + } + } + + fun typeAlias(name: ClassId) = typeAliases2.getOrPut(name) { + requireNotNull(plugin.referenceTypeAlias(name)) { "Could not find type alias $name" } + } + + + fun typeAliasUnwrapped(name: ClassId): IrType = typeAlias(name).owner.expandedType +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/symbols/PackageNames.kt b/knee-compiler-plugin/src/main/kotlin/symbols/PackageNames.kt new file mode 100644 index 0000000..affb943 --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/symbols/PackageNames.kt @@ -0,0 +1,19 @@ +package io.deepmedia.tools.knee.plugin.compiler.symbols + +import org.jetbrains.kotlin.name.FqName + +object PackageNames { + val kotlin = FqName("kotlin") + val kotlinCollections = FqName("kotlin.collections") + val kotlinCoroutines = FqName("kotlin.coroutines") + val cinterop = FqName("kotlinx.cinterop") + val platformAndroid = FqName("platform.android") + val annotations = FqName("io.deepmedia.tools.knee.annotations") + val runtime = FqName("io.deepmedia.tools.knee.runtime") + val runtimeCompiler = FqName("io.deepmedia.tools.knee.runtime.compiler") + val runtimeTypes = FqName("io.deepmedia.tools.knee.runtime.types") + val runtimeCollections = FqName("io.deepmedia.tools.knee.runtime.collections") + val runtimeBuffer = FqName("io.deepmedia.tools.knee.runtime.buffer") + val runtimeModule = FqName("io.deepmedia.tools.knee.runtime.module") +} + diff --git a/knee-compiler-plugin/src/main/kotlin/symbols/SymbolIds.kt b/knee-compiler-plugin/src/main/kotlin/symbols/SymbolIds.kt new file mode 100644 index 0000000..67e80d8 --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/symbols/SymbolIds.kt @@ -0,0 +1,94 @@ +package io.deepmedia.tools.knee.plugin.compiler.symbols + +import org.jetbrains.kotlin.name.CallableId +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.name.Name + +object KotlinIds { + val UInt = ClassId(PackageNames.kotlin, Name.identifier("UInt")) + val ULong = ClassId(PackageNames.kotlin, Name.identifier("ULong")) + val UByte = ClassId(PackageNames.kotlin, Name.identifier("UByte")) + val toUInt = CallableId(PackageNames.kotlin, Name.identifier("toUInt")) + val toULong = CallableId(PackageNames.kotlin, Name.identifier("toULong")) + val toUByte = CallableId(PackageNames.kotlin, Name.identifier("toUByte")) + fun FunctionX(x: Int) = ClassId(PackageNames.kotlin, Name.identifier("Function$x")) // this is in builtins too, but it crashes there + fun SuspendFunctionX(x: Int) = ClassId(PackageNames.kotlinCoroutines, Name.identifier("SuspendFunction$x")) // this is in builtins too, but it crashes there + val listOf = CallableId(PackageNames.kotlinCollections, Name.identifier("listOf")) + val error = CallableId(PackageNames.kotlin, Name.identifier("error")) + val Throwable = ClassId(PackageNames.kotlin, Name.identifier("Throwable")) +} + +// knee-annotations +object AnnotationIds { + val KneeMetadata = ClassId(PackageNames.annotations, Name.identifier("KneeMetadata")) + val Knee = FqName("io.deepmedia.tools.knee.annotations.Knee") + val KneeEnum = FqName("io.deepmedia.tools.knee.annotations.KneeEnum") + val KneeClass = FqName("io.deepmedia.tools.knee.annotations.KneeClass") + val KneeInterface = FqName("io.deepmedia.tools.knee.annotations.KneeInterface") + val KneeRaw = FqName("io.deepmedia.tools.knee.annotations.KneeRaw") +} + +object CInteropIds { + val CPointer = ClassId(PackageNames.cinterop, Name.identifier("CPointer")) + val staticCFunction = CallableId(PackageNames.cinterop, Name.identifier("staticCFunction")) + val COpaquePointer = ClassId(PackageNames.cinterop, Name.identifier("COpaquePointer")) +} + +object JDKIds { + fun NioBuffer(type: String) = FqName("java.nio.${type}Buffer") +} + +object PlatformIds { + val jobject = ClassId(PackageNames.platformAndroid, Name.identifier("jobject")) + val jobjectArray = ClassId(PackageNames.platformAndroid, Name.identifier("jobjectArray")) + val jclass = ClassId(PackageNames.platformAndroid, Name.identifier("jclass")) + val JNIEnvVar = ClassId(PackageNames.platformAndroid, Name.identifier("JNIEnvVar")) +} + +// knee-runtime +object RuntimeIds { + val initKnee = CallableId(PackageNames.runtime, Name.identifier("initKnee")) + val JNINativeMethod = ClassId(PackageNames.runtime, Name.identifier("JniNativeMethod")) + val useEnv = CallableId(PackageNames.runtime, Name.identifier("useEnv")) + fun callStaticMethod(type: String) = CallableId(PackageNames.runtime, Name.identifier("callStatic${type}Method")) + + val encodeClass = CallableId(PackageNames.runtimeTypes, Name.identifier("encodeClass")) + val decodeClass = CallableId(PackageNames.runtimeTypes, Name.identifier("decodeClass")) + val encodeEnum = CallableId(PackageNames.runtimeTypes, Name.identifier("encodeEnum")) + val decodeEnum = CallableId(PackageNames.runtimeTypes, Name.identifier("decodeEnum")) + val encodeString = CallableId(PackageNames.runtimeTypes, Name.identifier("encodeString")) + val decodeString = CallableId(PackageNames.runtimeTypes, Name.identifier("decodeString")) + val encodeBoolean = CallableId(PackageNames.runtimeTypes, Name.identifier("encodeBoolean")) + val decodeBoolean = CallableId(PackageNames.runtimeTypes, Name.identifier("decodeBoolean")) + val encodeInterface = CallableId(PackageNames.runtimeTypes, Name.identifier("encodeInterface")) + val decodeInterface = CallableId(PackageNames.runtimeTypes, Name.identifier("decodeInterface")) + fun encodeBoxed(type: String) = CallableId(PackageNames.runtimeTypes, Name.identifier("encodeBoxed${type}")) + fun decodeBoxed(type: String) = CallableId(PackageNames.runtimeTypes, Name.identifier("decodeBoxed${type}")) + + val JObjectCollectionCodec = ClassId(PackageNames.runtimeCollections, Name.identifier("JObjectCollectionCodec")) + val TransformingCollectionCodec = ClassId(PackageNames.runtimeCollections, Name.identifier("TransformingCollectionCodec")) + val typedArraySpec = CallableId(PackageNames.runtimeCollections, Name.identifier("typedArraySpec")) + fun PrimitiveCollectionCodec(type: String) = ClassId(PackageNames.runtimeCollections, Name.identifier("${type}CollectionCodec")) + fun PrimitiveArraySpec(type: String) = ClassId(PackageNames.runtimeCollections, FqName("ArraySpec.${type}s"), false) + + val KneeModule = ClassId(PackageNames.runtimeModule, Name.identifier("KneeModule")) + val KneeModule_getExportAdapter = CallableId(KneeModule, Name.identifier("getExportAdapter")) + private val KneeModuleBuilder = ClassId(PackageNames.runtimeModule, Name.identifier("KneeModuleBuilder")) + val KneeModuleBuilder_export = CallableId(KneeModuleBuilder, Name.identifier("export")) + val KneeModuleBuilder_exportAdapter = CallableId(KneeModuleBuilder, Name.identifier("exportAdapter")) + + val Adapter = KneeModule.createNestedClassId(Name.identifier("Adapter")) + val Adapter_decode = CallableId(Adapter, Name.identifier("decode")) + val Adapter_encode = CallableId(Adapter, Name.identifier("encode")) + + fun PrimitiveBuffer(type: String) = ClassId(PackageNames.runtimeBuffer, Name.identifier("${type}Buffer")) + + val KneeSuspendInvoker = FqName("io.deepmedia.tools.knee.runtime.compiler.KneeSuspendInvoker") + val KneeSuspendInvocation = FqName("io.deepmedia.tools.knee.runtime.compiler.KneeSuspendInvocation") + val kneeInvokeJvmSuspend = CallableId(PackageNames.runtimeCompiler, Name.identifier("kneeInvokeJvmSuspend")) + val kneeInvokeKnSuspend = CallableId(PackageNames.runtimeCompiler, Name.identifier("kneeInvokeKnSuspend")) + val JvmInterfaceWrapper = ClassId(PackageNames.runtimeCompiler, Name.identifier("JvmInterfaceWrapper")) + val rethrowNativeException = CallableId(PackageNames.runtimeCompiler, Name.identifier("rethrowNativeException")) + val SerializableException = ClassId(PackageNames.runtimeCompiler, Name.identifier("SerializableException")) +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/utils/IrUtils.kt b/knee-compiler-plugin/src/main/kotlin/utils/IrUtils.kt new file mode 100644 index 0000000..1a4e60e --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/utils/IrUtils.kt @@ -0,0 +1,308 @@ +package io.deepmedia.tools.knee.plugin.compiler.utils + +import io.deepmedia.tools.knee.plugin.compiler.context.KneeContext +import io.deepmedia.tools.knee.plugin.compiler.context.KneeOrigin +import io.deepmedia.tools.knee.plugin.compiler.symbols.KneeSymbols +import io.deepmedia.tools.knee.plugin.compiler.symbols.KotlinIds +import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext +import org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder +import org.jetbrains.kotlin.descriptors.ClassKind +import org.jetbrains.kotlin.descriptors.DescriptorVisibilities +import org.jetbrains.kotlin.descriptors.Modality +import org.jetbrains.kotlin.ir.builders.* +import org.jetbrains.kotlin.ir.builders.declarations.* +import org.jetbrains.kotlin.ir.declarations.* +import org.jetbrains.kotlin.ir.expressions.IrExpression +import org.jetbrains.kotlin.ir.expressions.IrFunctionExpression +import org.jetbrains.kotlin.ir.expressions.IrStatementOrigin +import org.jetbrains.kotlin.ir.expressions.impl.IrFunctionExpressionImpl +import org.jetbrains.kotlin.ir.types.* +import org.jetbrains.kotlin.ir.util.* +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.types.Variance + +fun IrType.simple(info: String): IrSimpleType { + return checkNotNull(this as? IrSimpleType) { "$info: type $this is not IrSimpleType."} +} + +fun IrDeclaration.isPartOf(module: IrModuleFragment): Boolean { + return runCatching { file.module == module }.getOrElse { false } +} + +fun IrFunction.requireNotComplex(description: Any, allowSuspend: Boolean = false) { + require(typeParameters.isEmpty()) { "$description can't have type parameters." } + require(extensionReceiverParameter == null) { "$description can't be an extension function." } + require(contextReceiverParametersCount == 0) { "$description can't have context receivers." } + require(allowSuspend || !isSuspend) { "$description can't be suspend." } + require(!isExpect) { "$description can't be an expect function, please annotate the actual function instead." } +} + +fun IrClass.requireNotComplex( + description: Any, + kind: ClassKind?, + // allowNested: Boolean = true, + typeArguments: List = emptyList() +) { + if (typeParameters.isNotEmpty()) { + require(typeArguments.size == typeParameters.size) { "$description can't have unmatched type parameters." } + typeArguments.forEach { + when (it) { + is IrTypeProjection -> { + require(!it.type.isTypeParameter()) { "Type parameter ${it.type} of $description can't be a generic type coming from some parent declaration." } + require(it.variance == Variance.INVARIANT) { "Type parameter ${it.type} of $description must be invariant, was ${it.variance}." } + } + is IrStarProjection -> error("Type parameter $it can't be a wildcard.") + else -> error("Should not happen? ${it::class.simpleName}") + } + } + } + require(!isInner) { "$description can't be an inner class." } + require(kind == null || this.kind == kind) { "$description must be a $kind (was ${this.kind})." } + if (kind != null && kind != ClassKind.INTERFACE) { + require(modality != Modality.ABSTRACT) { "$description can't be an abstract class" } + } + // require(allowNested || parentClassOrNull == null) { "$this can't be contained in another class." } + require(!isExpect) { "$description can't be an expect class, please annotate the actual class instead." } +} + +/** + * Writing properties is tricky. + * A simple property here is a 'val' with default getter and some initializer. + */ +fun IrDeclarationContainer.addSimpleProperty( + plugin: IrPluginContext, + type: IrType, + name: Name, + initializer: IrBuilderWithScope.() -> IrExpression +): IrProperty { + val parent = this + val property = plugin.irFactory.buildProperty { + isVar = false + origin = KneeOrigin.KNEE + this.name = name + } + property.apply { + this.parent = parent + backingField = factory.buildField { + isStatic = parent is IrFile || (parent is IrDeclaration && parent.isFileClass) // very important + origin = IrDeclarationOrigin.PROPERTY_BACKING_FIELD + this.name = name + this.type = type + }.apply { + correspondingPropertySymbol = property.symbol + this.parent = parent + this.initializer = DeclarationIrBuilder(plugin, symbol).run { + irExprBody(initializer()) + } + } + addGetter { + origin = IrDeclarationOrigin.DEFAULT_PROPERTY_ACCESSOR + returnType = backingField!!.type + }.apply { + body = DeclarationIrBuilder(plugin, symbol).irBlockBody { + +irReturn(irGetField(null, backingField!!)) + } + } + } + declarations.add(property) + return property +} + +/** + * This is one of the two ways of passing a lambda in IR code. + * It's equivalent to, for example, list.map { ... }. We proceed by creating a lambda whose parent is inferred + * from the current scope, typically it is the parent function. Note that as far as I can see the lambda itself is + * not added as a child of the parent, so the wiring only goes in one direction. + * + * Lambda should have the [IrDeclarationOrigin.LOCAL_FUNCTION_FOR_LAMBDA] origin + * and [DescriptorVisibilities.LOCAL] visibility. + * + * Then we create a [IrFunctionExpression] out of it, which makes it a lambda. + * Something similar is done in CStructVarClassGenerator.kt in kotlin source code (1.7.0? 1.6.21?). + * https://github.com/JetBrains/kotlin/blob/9148094bbdc53d4c5cfb16bebab41bc5f561e19a/[…]in/backend/konan/ir/interop/cstruct/CStructVarClassGenerator.kt + * Sample IR code: + * + * fun functionAcceptingALambda(lambdaArgument: (Int) -> Int) = ... + * fun main() { + * functionAcceptingALambda { it + 1 } + * } + * + * lambdaArgument: FUN_EXPR type=kotlin.Function1 origin=LAMBDA + * FUN LOCAL_FUNCTION_FOR_LAMBDA name: visibility:local modality:FINAL <> (it:kotlin.Int) returnType:kotlin.Int + * VALUE_PARAMETER name:it index:0 type:kotlin.Int + * BLOCK_BODY + * ... lambda block body + */ +fun irLambda( + context: KneeContext, + parent: IrDeclarationParent, + valueParameters: List, + returnType: IrType, + suspend: Boolean = false, + content: IrBlockBodyBuilder.(IrSimpleFunction) -> Unit +): IrFunctionExpression = irLambda( + context = context, + parent = parent, + suspend = suspend, + content = { + it.returnType = returnType + valueParameters.forEachIndexed { i, type -> it.addValueParameter("arg$i", type) } + content(it) + } +) + +fun irLambda( + context: KneeContext, + parent: IrDeclarationParent, + suspend: Boolean = false, + content: IrBlockBodyBuilder.(IrSimpleFunction) -> Unit +): IrFunctionExpression { + val lambda = context.factory.buildFun { + startOffset = SYNTHETIC_OFFSET + endOffset = SYNTHETIC_OFFSET + origin = IrDeclarationOrigin.LOCAL_FUNCTION_FOR_LAMBDA + name = Name.special("") + visibility = DescriptorVisibilities.LOCAL + isSuspend = suspend + }.apply { + this.parent = parent + body = DeclarationIrBuilder(context.plugin, this.symbol, SYNTHETIC_OFFSET, SYNTHETIC_OFFSET).irBlockBody { + content(this@apply) + } + } + return IrFunctionExpressionImpl( + startOffset = SYNTHETIC_OFFSET, + endOffset = SYNTHETIC_OFFSET, + type = run { + when (suspend) { + false -> context.symbols.klass(KotlinIds.FunctionX(lambda.valueParameters.size)) + true -> context.symbols.klass(KotlinIds.SuspendFunctionX(lambda.valueParameters.size)) + // true -> context.irBuiltIns.suspendFunctionN(lambda.valueParameters.size) + }.typeWith(*lambda.valueParameters.map { it.type }.toTypedArray(), lambda.returnType) + }, + origin = IrStatementOrigin.LAMBDA, + function = lambda + ) +} + +fun IrBuilderWithScope.irError(symbols: KneeSymbols, message: String): IrExpression { + val error = symbols.functions(KotlinIds.error).single() + return irCall(error).apply { + putValueArgument(0, irString(message)) + } +} + + +// Not IrType.hashCode because it seems that it's not stable in some scenarios +// No, this doesn't seem stable either, resorting to different strategies +/* fun IrType.disambiguationHash(): Int { + val data = mutableListOf().also { disambiguationEntries(it) } + return data.hashCode() // .also { println("disambiguation of $this: $it <- ${data.joinToString(separator = "")}") } +} + +private fun IrType.disambiguationEntries(list: MutableList) { + if (this !is IrSimpleType) { + error("Can't compute disambiguationHash because $this is not a IrSimpleType ($list).") + } + list.add(classOrNull?.owner?.classId?.asSingleFqName()?.asString() ?: "null") + list.add(arguments.size.toString()) + if (arguments.isNotEmpty()) { + list.add("{") + arguments.forEach { + when (it) { + is IrTypeProjection -> { + list.add(it.variance.name) + it.type.disambiguationEntries(list) + } + is IrStarProjection -> list.add("*") + // else -> list.add(it) + else -> error("Can't compute disambiguationHash because $it is not a IrTypeProjection/IrStarProjection ($list).") + } + list.add(",") + } + list.add("}") + } +} */ + + +/** + * When migrating a declaration to top level, name must be made unique + * within that package, so we can't simply use the declaration name as is. + */ +// val IrDeclarationWithName.uniqueName: Name get() = uniqueName { it } +/* inline fun IrDeclarationWithName.uniqueName(special: Boolean = name.isSpecial, map: (String) -> String): Name { + val prefix = when (val p = parent) { + is IrDeclarationWithName -> p.fqNameWithoutPackageName.pathSegments().joinToString("_") { + require(!it.isSpecial) { "Ancestor of $name, $it has special characters. Not sure how to handle this." } + it.asString() + } + is IrPackageFragment -> null + else -> error("Parent of $name is invalid: $parent") + } + return name.map(special) { + listOfNotNull(prefix, map(it)).joinToString("_") + } +} */ + + +/** + * Creates a local function, like in: + * fun main() { + * fun doStuff(int: Int) = int + 1 + * println(doStuff(41)) + * } + * + * The function should have: + * - A name + * - Origin: [IrDeclarationOrigin.LOCAL_FUNCTION] + * - Visibility: [DescriptorVisibilities.LOCAL] + * Sample internal representation: + * + * FUN LOCAL_FUNCTION name:mapIntToInt visibility:local modality:FINAL <> (int:kotlin.Int) returnType:kotlin.Int + * VALUE_PARAMETER name:int index:0 type:kotlin.Int + * BLOCK_BODY + * RETURN type=kotlin.Nothing from='local final fun mapIntToInt (int: kotlin.Int): kotlin.Int declared in io.deepmedia.tools.knee.sample.xxx' + * CALL 'public final fun plus (other: kotlin.Int): kotlin.Int [external,operator] declared in kotlin.Int' type=kotlin.Int origin=PLUS + * $this: GET_VAR 'int: kotlin.Int declared in io.deepmedia.tools.knee.sample.xxx.mapIntToInt' type=kotlin.Int origin=null + * other: CONST Int type=kotlin.Int value=1 + * + * Note that the function can be passed to other functions accepting a lambda by using [irFunctionReference] on it. + * In this case at usage site we will see: + * lambda: FUNCTION_REFERENCE 'local final fun mapIntToInt (int: kotlin.Int): kotlin.Int declared in io.deepmedia.tools.knee.sample.xxx' type=kotlin.reflect.KFunction1 origin=null reflectionTarget= + */ +/*fun IrFactory.buildLocalFun( + parent: IrFunction, + name: Name, + suspend: Boolean = false, + returnType: IrType +): IrSimpleFunction { + return buildFun { + startOffset = SYNTHETIC_OFFSET + endOffset = SYNTHETIC_OFFSET + origin = IrDeclarationOrigin.LOCAL_FUNCTION + this.name = name + visibility = DescriptorVisibilities.LOCAL + isSuspend = suspend + this.returnType = returnType + }.apply { + this.parent = parent + } +}*/ + +/* @Suppress("RecursivePropertyAccessor") +val IrDeclarationWithName.fqNameWithoutPackageName: FqName + get() = when (val parent = parent) { + is IrDeclarationWithName -> parent.fqNameWithoutPackageName.child(name) + is IrPackageFragment -> FqName(name.asString()) + else -> error("Parent of $name is invalid: $parent") + } + + +fun IrBuilderWithScope.irFunctionReference(type: IrType, function: IrFunction) = irFunctionReference( + type = type, + symbol = function.symbol, + typeArgumentsCount = function.typeParameters.size, + valueArgumentsCount = function.valueParameters.size +) +*/ + diff --git a/knee-compiler-plugin/src/main/kotlin/utils/NameUtils.kt b/knee-compiler-plugin/src/main/kotlin/utils/NameUtils.kt new file mode 100644 index 0000000..3b14bc8 --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/utils/NameUtils.kt @@ -0,0 +1,115 @@ +package io.deepmedia.tools.knee.plugin.compiler.utils + +import io.deepmedia.tools.knee.plugin.compiler.symbols.AnnotationIds +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.descriptors.ModuleDescriptor +import org.jetbrains.kotlin.descriptors.PackageFragmentDescriptor +import org.jetbrains.kotlin.ir.declarations.IrClass +import org.jetbrains.kotlin.ir.declarations.IrDeclarationWithName +import org.jetbrains.kotlin.ir.declarations.IrPackageFragment +import org.jetbrains.kotlin.ir.expressions.IrConst +import org.jetbrains.kotlin.ir.types.IrType +import org.jetbrains.kotlin.ir.types.classOrNull +import org.jetbrains.kotlin.ir.util.getAnnotation +import org.jetbrains.kotlin.ir.util.getValueArgument +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.resolve.constants.StringValue +import org.jetbrains.kotlin.resolve.descriptorUtil.parentsWithSelf + +// other utils + +fun Name.asStringSafeForCodegen(firstLetterLowercase: Boolean): String { + // KotlinPoet has a very restrictive regex for things added to CodeSpec.Builder.addNamed + // and the input it.name can be extremely weird like "". + return asStringStripSpecialMarkers() + .filter { it.isLetter() || it == '_' || it.isDigit() } + .replaceFirstChar { if (firstLetterLowercase) it.lowercase() else it.uppercase() } +} + +inline fun Name.map(special: Boolean = isSpecial, block: (String) -> String): Name { + val name = block(asStringStripSpecialMarkers()) + return if (!special) Name.identifier(name) else Name.special("<$name>") +} + +// IR => codegen name + +val IrDeclarationWithName.codegenFqName: FqName + get() = when (val parent = parent) { + is IrDeclarationWithName -> parent.codegenFqName.child(codegenName) + is IrPackageFragment -> parent.packageFqName.child(codegenName) + else -> FqName(codegenName.asString()) + } + +val IrType.codegenClassFqName: FqName? + get() = classOrNull?.owner?.codegenFqName + +val IrClass.codegenClassId: ClassId? + get() = when (val parent = this.parent) { + is IrClass -> parent.codegenClassId?.createNestedClassId(this.codegenName) + is IrPackageFragment -> ClassId.topLevel(parent.packageFqName.child(this.codegenName)) + else -> null + } + + +val IrDeclarationWithName.codegenFqNameWithoutPackageName: FqName + get() = when (val parent = parent) { + is IrDeclarationWithName -> parent.codegenFqNameWithoutPackageName.child(codegenName) + is IrPackageFragment -> FqName(codegenName.asString()) + else -> error("Parent of $codegenName is invalid: $parent") + } + + +@Suppress("UNCHECKED_CAST") +val IrDeclarationWithName.codegenName get(): Name { + if (this is IrClass) { + val e = getAnnotation(AnnotationIds.KneeClass) + ?: getAnnotation(AnnotationIds.KneeEnum) + ?: getAnnotation(AnnotationIds.KneeInterface) + ?: return name + val a = e.getValueArgument(Name.identifier("name")) ?: return name + val str = (a as IrConst).value.takeIf { it.isNotEmpty() } ?: return name + return Name.identifier(str) + } + return name +} + +inline fun IrDeclarationWithName.codegenUniqueName(special: Boolean = codegenName.isSpecial, map: (String) -> String): Name { + val prefix = when (val p = parent) { + is IrDeclarationWithName -> p.codegenFqNameWithoutPackageName.pathSegments().joinToString("_") { + require(!it.isSpecial) { "Ancestor of $codegenName, $it has special characters. Not sure how to handle this." } + it.asString() + } + is IrPackageFragment -> null + else -> error("Parent of $codegenName is invalid: $parent") + } + return codegenName.map(special) { + listOfNotNull(prefix, map(it)).joinToString("_") + } +} + +// FIR => codegen name + +val DeclarationDescriptor.codegenName: Name + get() { + val e = annotations.findAnnotation(AnnotationIds.KneeClass) + ?: annotations.findAnnotation(AnnotationIds.KneeEnum) + ?: annotations.findAnnotation(AnnotationIds.KneeInterface) + ?: return name + val a = e.allValueArguments[Name.identifier("name")] ?: return name + val str = (a as StringValue).value.takeIf { it.isNotEmpty() } ?: return name + return Name.guessByFirstCharacter(str) +} + +val DeclarationDescriptor.codegenFqName: FqName + get() { + val segments = parentsWithSelf.mapNotNull { + when (it) { + is ModuleDescriptor -> null + is PackageFragmentDescriptor -> Name.identifier(it.fqName.asString()) + else -> codegenName + } + }.toList().reversed() + return FqName(segments.joinToString(separator = ".")) +} \ No newline at end of file diff --git a/knee-compiler-plugin/src/main/kotlin/utils/PoetUtils.kt b/knee-compiler-plugin/src/main/kotlin/utils/PoetUtils.kt new file mode 100644 index 0000000..1704da2 --- /dev/null +++ b/knee-compiler-plugin/src/main/kotlin/utils/PoetUtils.kt @@ -0,0 +1,236 @@ +package io.deepmedia.tools.knee.plugin.compiler.utils + +import com.squareup.kotlinpoet.* +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import org.jetbrains.kotlin.descriptors.* +import org.jetbrains.kotlin.ir.declarations.* +import org.jetbrains.kotlin.ir.expressions.IrConst +import org.jetbrains.kotlin.ir.expressions.IrConstKind +import org.jetbrains.kotlin.ir.expressions.IrExpressionBody +import org.jetbrains.kotlin.ir.symbols.IrClassSymbol +import org.jetbrains.kotlin.ir.symbols.IrTypeParameterSymbol +import org.jetbrains.kotlin.ir.types.* +import org.jetbrains.kotlin.types.Variance + + +fun IrClass.asTypeSpec(rename: ((String) -> String)? = null): TypeSpec.Builder { + // NOTE: Could also add kmodifiers from visibility and modality + val name = codegenName.map { rename?.invoke(it) ?: it }.asString() + return when (kind) { + ClassKind.ENUM_CLASS -> TypeSpec.enumBuilder(name) + ClassKind.OBJECT -> TypeSpec.objectBuilder(name) + ClassKind.INTERFACE -> TypeSpec.interfaceBuilder(name) + ClassKind.ANNOTATION_CLASS -> TypeSpec.annotationBuilder(name) + ClassKind.ENUM_ENTRY -> error("Enum entries ($this) can't become a TypeSpec.") + ClassKind.CLASS -> TypeSpec.classBuilder(name) + } +} + +/** + * Adding special logic for recursive types, as in: + * interface ClosedRange> + * When calling Comparable.asTypeName(), we handle Comparable easily, then find T and + * start looking for T's bounds if any in its super types. But T's only super type + * is set to Comparable which means we will stack overflow. + */ +fun IrSimpleType.asTypeName(alreadyDescribedTypeParameters: MutableSet = mutableSetOf()): TypeName { + // IrClassifierSymbol can be IrClassSymbol, IrScriptSymbol or IrTypeParameterSymbol + + return when (val s = classifier) { + is IrClassSymbol -> { + asClassTypeName(alreadyDescribedTypeParameters) + } + is IrTypeParameterSymbol -> TypeVariableName( + name = s.owner.name.asString(), + bounds = when { + alreadyDescribedTypeParameters.add(s) -> emptyList() + else -> s.owner.superTypes + .filterIsInstance() + .map { it.asTypeName(alreadyDescribedTypeParameters) } + } + ).copy(nullable = nullability == SimpleTypeNullability.MARKED_NULLABLE) + else -> error("Unexpected classifier: $s") + } +} + +private fun IrTypeArgument.asTypeName(alreadyDescribedTypeParameters: MutableSet): TypeName { + return when (this) { + is IrTypeProjection -> { + val simpleType = checkNotNull(type as? IrSimpleType) { "IrTypeArgument.type not a simple type: $type" } + val invariant = simpleType.asTypeName(alreadyDescribedTypeParameters) + when (this.variance) { + Variance.INVARIANT -> invariant + Variance.IN_VARIANCE -> WildcardTypeName.consumerOf(inType = invariant) + Variance.OUT_VARIANCE -> WildcardTypeName.producerOf(outType = invariant) + } + } + is IrStarProjection -> STAR + else -> error("Should not happen? ${this::class.simpleName}") + } +} + +private fun IrSimpleType.asClassTypeName(alreadyDescribedTypeParameters: MutableSet): TypeName { + val fqName = requireNotNull(codegenClassFqName) { + "IrType $this can't become a TypeName (classFqName is: ${classFqName}, classifier: ${classifier::class.simpleName})" + } + val className = ClassName.bestGuess(fqName.asString()) + return when (arguments.isEmpty()) { + true -> className + else -> className.parameterizedBy(arguments.map { it.asTypeName(alreadyDescribedTypeParameters) }) + }.copy(nullable = isNullable()) +} + + +fun IrProperty.asPropertySpec(typeMapper: (IrSimpleType) -> IrSimpleType = { it }): PropertySpec.Builder { + // NOTE: Could also add kmodifiers from visibility and modality + val type = requireNotNull(backingField?.type ?: getter?.returnType) + val simpleType = checkNotNull(type as? IrSimpleType) { "IrProperty.type not a simple type: $type" } + val mappedType = typeMapper(simpleType) + return PropertySpec.builder(codegenName.asString(), mappedType.asTypeName()) + .mutable(isVar) +} + +fun DescriptorVisibility.asModifier(): KModifier { + return when (delegate) { + Visibilities.Public -> KModifier.PUBLIC + Visibilities.Private -> KModifier.PRIVATE + Visibilities.Protected -> KModifier.PROTECTED + Visibilities.Local -> KModifier.PRIVATE + Visibilities.Internal -> KModifier.INTERNAL + Visibilities.InvisibleFake -> KModifier.PRIVATE + Visibilities.PrivateToThis -> KModifier.PRIVATE + Visibilities.Unknown -> KModifier.INTERNAL + Visibilities.Inherited -> { + // No idea whether this is correct + return (this as? IrOverridableDeclaration<*>) + ?.overriddenSymbols + ?.map { it.owner } + ?.filterIsInstance() + ?.firstOrNull { it.visibility.delegate != Visibilities.Inherited } + ?.visibility + ?.asModifier() ?: KModifier.PRIVATE + } + else -> KModifier.INTERNAL + } + // K1 + /* val effective = visibility.effectiveVisibility(descriptor = descriptor, checkPublishedApi = false) + return when (visibility.delegate) { + is EffectiveVisibility.Local, + is EffectiveVisibility.PrivateInClass, + is EffectiveVisibility.PrivateInFile -> KModifier.PRIVATE + is EffectiveVisibility.InternalOrPackage -> KModifier.INTERNAL + is EffectiveVisibility.Public -> KModifier.PUBLIC + is EffectiveVisibility.Protected, + is EffectiveVisibility.ProtectedBound, + is EffectiveVisibility.InternalProtected, + is EffectiveVisibility.InternalProtectedBound -> KModifier.PROTECTED + is EffectiveVisibility.Unknown -> KModifier.PRIVATE // can't be inferred + } */ +} + +val TypeName.simpleName: String get() { + return when (this) { + is ClassName -> simpleName + is ParameterizedTypeName -> rawType.simpleName + is Dynamic -> error("Not possible") // JS dynamic type + is TypeVariableName -> error("Not possible") // describes generic named type parameter e.g. 'T : String' + is LambdaTypeName -> error("Not possible") // describes lambdas + is WildcardTypeName -> error("Not possible") // describes out String, in String, * ... + } +} + +val TypeName.canonicalName: String get() { + return when (this) { + is ClassName -> canonicalName + is ParameterizedTypeName -> rawType.canonicalName + is Dynamic, is LambdaTypeName, is TypeVariableName, is WildcardTypeName -> error("Not possible: ${this}") + } +} + +// Wrt canonicalName, this handles TypeVariableName. +// That can appear when creating codegen function for generic interfaces, because we use +// unsubstituted types in the base interface clone +val TypeName.disambiguationName: String get() { + return when (this) { + is ClassName -> canonicalName + is ParameterizedTypeName -> rawType.canonicalName + is TypeVariableName -> "${if (isReified) "reified " else ""}$variance $name : ${bounds.map { it.disambiguationName }}" + is Dynamic, is LambdaTypeName, is WildcardTypeName -> error("Not possible: ${this}") + } +} + +/* val TypeName.packageName: String get() { + return when (this) { + is ClassName -> packageName + is ParameterizedTypeName -> rawType.packageName + is Dynamic, is LambdaTypeName, is TypeVariableName, is WildcardTypeName -> error("Not possible") + } +} */ + +/* fun TypeName.copy( + packageName: String = this.packageName, + simpleName: String = this.simpleName +): TypeName { + return when (this) { + is ClassName -> { + val simpleNames = simpleNames.toMutableList() + simpleNames[simpleNames.lastIndex] = simpleName + ClassName(packageName, simpleNames) + } + is ParameterizedTypeName -> (rawType.copy(packageName, simpleName) as ClassName).parameterizedBy(typeArguments) + is Dynamic, is LambdaTypeName, is TypeVariableName, is WildcardTypeName -> error("Not possible") + } +} */ + +fun TypeName.copy( + clearGenerics: Boolean = false, + wildcardGenerics: Boolean = false // useful for 'is' checks +): TypeName { + return when (this) { + is ClassName -> this + is ParameterizedTypeName -> when { + clearGenerics -> rawType + wildcardGenerics -> copy(typeArguments = List(typeArguments.size) { STAR }) + else -> this + } + is Dynamic, is LambdaTypeName, is TypeVariableName, is WildcardTypeName -> error("Not possible") + } +} + +// We only support const kinds. +fun IrValueParameter.defaultValueForCodegen(functionExpects: List = emptyList()): CodeBlock? { + val expression = (defaultValueFromThisOrSupertypes ?: defaultValueFromExpect(functionExpects) ?: return null).expression + if (expression is IrConst<*>) { + return when (val kind = expression.kind) { + is IrConstKind.Null -> CodeBlock.of("null") + is IrConstKind.String -> CodeBlock.of(kind.valueOf(expression)) + else -> CodeBlock.of("%L", kind.valueOf(expression)) + // is IrConstKind.Boolean -> CodeBlock.of(kind.valueOf(expression).toString()) + // is IrConstKind.Int -> CodeBlock.of(kind.valueOf(expression).toString()) + // is IrConstKind.Double -> CodeBlock.of(kind.valueOf(expression).toString()) + // is IrConstKind.Float -> CodeBlock.of(kind.valueOf(expression).toString() + "F") + // is IrConstKind.Long -> CodeBlock.of(kind.valueOf(expression).toString() + "L") + // else -> return null + } + } + // risky option: take expression.dumpKotlinLike() as string. + return null +} + +private val IrValueParameter.defaultValueFromThisOrSupertypes: IrExpressionBody? get() { + if (defaultValue != null) return defaultValue + val parent = parent as? IrSimpleFunction ?: return null + return parent.overriddenSymbols.asSequence() + .map { it.owner } + .mapNotNull { it.valueParameters.firstOrNull { it.name == name } } + .firstNotNullOfOrNull { it.defaultValueFromThisOrSupertypes } +} + + +private fun IrValueParameter.defaultValueFromExpect(functionExpects: List): IrExpressionBody? { + return functionExpects.asSequence() + .filterIsInstance() + .mapNotNull { it.valueParameters.firstOrNull { it.name == name } } + .firstNotNullOfOrNull { it.defaultValueFromThisOrSupertypes } +} + diff --git a/knee-compiler-plugin/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor b/knee-compiler-plugin/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor new file mode 100644 index 0000000..3debbfc --- /dev/null +++ b/knee-compiler-plugin/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor @@ -0,0 +1 @@ +io.deepmedia.tools.knee.plugin.compiler.services.KneeCommandLineProcessor diff --git a/knee-compiler-plugin/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar b/knee-compiler-plugin/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar new file mode 100644 index 0000000..9142d96 --- /dev/null +++ b/knee-compiler-plugin/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar @@ -0,0 +1 @@ +io.deepmedia.tools.knee.plugin.compiler.services.KneeComponentRegistrar diff --git a/knee-gradle-plugin/.gitignore b/knee-gradle-plugin/.gitignore new file mode 100644 index 0000000..f3d6549 --- /dev/null +++ b/knee-gradle-plugin/.gitignore @@ -0,0 +1 @@ +/build/ \ No newline at end of file diff --git a/knee-gradle-plugin/build.gradle.kts b/knee-gradle-plugin/build.gradle.kts new file mode 100644 index 0000000..6b85b0d --- /dev/null +++ b/knee-gradle-plugin/build.gradle.kts @@ -0,0 +1,76 @@ +import java.util.concurrent.Callable +plugins { + kotlin("jvm") + `kotlin-dsl` + `java-gradle-plugin` + id("io.deepmedia.tools.deployer") +} + +val injectProjectConfig by tasks.registering { + val kneeVersion = providers.gradleProperty("knee.version") + val kneeGroup = providers.gradleProperty("knee.group") + val file = layout.buildDirectory.get().dir("generated").dir("sources").dir("config").file("KneeConfig.kt").asFile + inputs.property("knee.config", Callable { kneeVersion.get() + kneeGroup.get() }) // Is this needed? Probably not + outputs.dir(file.parentFile) + doLast { + file.delete() + file.writeText(""" + // Generated file + package io.deepmedia.tools.knee.plugin.gradle + + internal const val KneeVersion = "${kneeVersion.get()}" + internal const val KneeGroup = "${kneeGroup.get()}" + """.trimIndent()) + } +} + +kotlin { + jvmToolchain(11) + sourceSets["main"].kotlin.srcDir(injectProjectConfig) +} + +/* sourceSets { + main { + java.srcDir("$buildDir/generated/sources/config/") + } +} +tasks.withType().configureEach { + dependsOn(injectProjectConfig) +} */ + +dependencies { + compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin") + compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin-api") + compileOnly("com.android.tools.build:gradle:8.1.1") +} + +gradlePlugin { + isAutomatedPublishing = true + plugins { + create("knee") { + id = "io.deepmedia.tools.knee" + implementationClass = "io.deepmedia.tools.knee.plugin.gradle.KneePlugin" + } + } +} + +deployer { + content.gradlePluginComponents { + kotlinSources() + emptyDocs() + } +} + +// Gradle 7.X has embedded kotlin version 1.6, but kotlin-dsl plugins are compiled with 1.4 for compatibility with older +// gradle versions (I guess). 1.4 is very old and generates a warning, so let's bump to the embedded kotlin version. +// https://handstandsam.com/2022/04/13/using-the-kotlin-dsl-gradle-plugin-forces-kotlin-1-4-compatibility/ +// https://github.com/gradle/gradle/blob/7a69f2f3d791044b946040cd43097ce57f430ca8/subprojects/kotlin-dsl-plugins/src/main/kotlin/org/gradle/kotlin/dsl/plugins/dsl/KotlinDslCompilerPlugins.kt#L48-L49 +/* afterEvaluate { + tasks.withType().configureEach { + kotlinOptions { + val embedded = embeddedKotlinVersion.split(".").take(2).joinToString(".") + apiVersion = embedded + languageVersion = embedded + } + } +} */ \ No newline at end of file diff --git a/knee-gradle-plugin/src/main/kotlin/KneeExtension.kt b/knee-gradle-plugin/src/main/kotlin/KneeExtension.kt new file mode 100644 index 0000000..304d807 --- /dev/null +++ b/knee-gradle-plugin/src/main/kotlin/KneeExtension.kt @@ -0,0 +1,53 @@ +package io.deepmedia.tools.knee.plugin.gradle + +import org.gradle.api.Project +import org.gradle.api.file.Directory +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.ProjectLayout +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.provider.ProviderFactory +import org.gradle.kotlin.dsl.property +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget +import org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType +import javax.inject.Inject + +abstract class KneeExtension @Inject constructor(objects: ObjectFactory, private val layout: ProjectLayout, private val providers: ProviderFactory) { + + private fun Property.conventions(key: String, fallback: Boolean): Property { + val env = providers.environmentVariable("io.deepmedia.knee.$key").map { it.toBoolean() } + val prop = providers.gradleProperty("io.deepmedia.knee.$key").map { it.toBoolean() } + return convention(prop.orElse(env).orElse(fallback)) + } + + val enabled: Property = objects.property().convention(true) + + val verboseLogs: Property = objects.property().conventions("verboseLogs", false) + + val verboseRuntime: Property = objects.property().conventions("verboseRuntime", false) + + val verboseSources: Property = objects.property().conventions("verboseSources", false) + + val connectTargets: Property = objects.property().conventions("connectTargets", true) + + /* val autoBind: Property = objects + .property() + .convention(false) */ + + val generatedSourceDirectory: DirectoryProperty = objects + .directoryProperty() + .convention(layout.buildDirectory.map { it.dir("knee").dir("src") }) + + fun generatedSourceDirectory(target: KotlinNativeTarget): Provider { + // note: this is possible because we only apply on the main compilation + // otherwise we'd have to add a main/test subdirectory/suffix for example. + return generatedSourceDirectory.dir(target.name) + } + + internal fun log(message: String) { + if (verboseLogs.get()) println("[KneePlugin] [${projectName}] $message") + } + + internal lateinit var projectName: String +} \ No newline at end of file diff --git a/knee-gradle-plugin/src/main/kotlin/KneePackaging.kt b/knee-gradle-plugin/src/main/kotlin/KneePackaging.kt new file mode 100644 index 0000000..8fb8f1c --- /dev/null +++ b/knee-gradle-plugin/src/main/kotlin/KneePackaging.kt @@ -0,0 +1,138 @@ +package io.deepmedia.tools.knee.plugin.gradle + +import tasks.UnpackageCodegenSources +import org.gradle.api.Project +import org.gradle.api.artifacts.Configuration +import org.gradle.api.attributes.Attribute +import org.gradle.api.internal.artifacts.transform.UnzipTransform +import org.gradle.api.tasks.TaskProvider +import org.gradle.api.tasks.bundling.Jar +import org.gradle.kotlin.dsl.* +import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget +import org.jetbrains.kotlin.konan.target.KonanTarget +import org.jetbrains.kotlin.konan.target.presetName + + +/** + * Provides support for multimodule codegen dependencies. + * - [registerPackageTask] must be called on producer modules + * - [registerUnpackageTask] must be called on the consumer module + * The package task will create a JAR with the JVM generated sources. The unpackage task will fetch and unzip such + * sources so that they can be added as a source set. + * + * The code here is a bit complex due to having to support two use cases: + * 1. remote repositories. In this case the package codegen task output should be added + * as a publishing artifact, e.g. packagename-1.0.0-codegen.jar so we can fetch it in frontend. + * 2. project(":dep") dependencies. In this case we must take care to add the [NativeTargetAttribute] + * and create outgoing configurations in the backend, which Gradle treats like variant in a published module. + * + * NOTE: a project should only register the package task if it does not have the JVM/Android targets. + * If it has Android targets, the codegen data can be included with [KneeExtension.connectTargets]. + */ +object KneePackaging { + + private const val Classifier = "knee-codegen" + private const val JarArtifactType = "knee-codegen-zipped" + private const val DirArtifactType = "knee-codegen-unzipped" + + // It's not strictly necessary, but let's reuse common attribute names like artifactType and "org.jetbrains.kotlin.native.target" + private val ArtifactTypeAttribute = Attribute.of("artifactType", String::class.java) + private val NativeTargetAttribute = Attribute.of("org.jetbrains.kotlin.native.target", String::class.java) + + private const val ConfigurationNamePrefix = "knee" + private const val ConfigurationNameSuffix = "Codegen" + + fun registerPackageTask( + target: KotlinNativeTarget + ): TaskProvider { + val project = target.project + val targetName = target.name.replaceFirstChar(Char::titlecase) + val konanName = target.konanTarget.name + val taskName = "packageKnee${targetName}Codegen" + + return runCatching { project.tasks.named(taskName, Jar::class.java) }.getOrNull() ?: project.tasks.register(taskName) { + // ${archiveBaseName}-${archiveAppendix}-${archiveVersion}-${archiveClassifier}.${archiveExtension} + val knee = project.extensions.getByType() + from(knee.generatedSourceDirectory(target)) + dependsOn(target.compilations[KotlinCompilation.MAIN_COMPILATION_NAME].compileKotlinTaskName) + archiveClassifier.set(Classifier) + archiveAppendix.set(target.name.lowercase()) + }.also { task -> + // stuff below is to support project(":dep") + val outgoing = project.configurations.create("$ConfigurationNamePrefix$targetName$ConfigurationNameSuffix") { + isCanBeResolved = false + isCanBeConsumed = true + attributes { + attribute(ArtifactTypeAttribute, JarArtifactType) + attribute(NativeTargetAttribute, konanName) + } + } + project.artifacts { + add(outgoing.name, task) { builtBy(task) } + } + } + } + + fun createUnpackageConfiguration( + project: Project, + architecture: KonanTarget + ): Configuration { + // Note: it should be possible to define this as unzipped DirArtifactType, but there's a gradle bug: + // https://github.com/gradle/gradle/issues/8386 so we're forced to use artifactView {} below instead. + val targetName = architecture.presetName.replaceFirstChar(Char::titlecase) + val konanName = architecture.name + val configuration = project.configurations.create("$ConfigurationNamePrefix$targetName$ConfigurationNameSuffix") { + isTransitive = false + isCanBeConsumed = false + isCanBeResolved = true + attributes { + attribute(ArtifactTypeAttribute, JarArtifactType) + attribute(NativeTargetAttribute, konanName) + } + } + + // Add a 'codegen' variant to every dependency, pointing at dep-1.0.0-`Classifier`.jar. + // Some of them won't have this jar, in which case adding it to the codegen configuration would throw. + // https://docs.gradle.org/current/userguide/component_metadata_rules.html#making_variants_published_as_classified_jars_explicit + project.dependencies { + components.all { + val details = this + // The variant name should not matter. + addVariant("knee-codegen") { + attributes { + attribute(ArtifactTypeAttribute, JarArtifactType) + attribute(NativeTargetAttribute, konanName) + } + withFiles { + removeAllFiles() + addFile("${details.id.name}-${details.id.version}-$Classifier.jar") + } + } + } + + // Add the ability to unzip the incoming jars. + registerTransform(UnzipTransform::class.java) { + from.attribute(ArtifactTypeAttribute, JarArtifactType) + to.attribute(ArtifactTypeAttribute, DirArtifactType) + } + } + + return configuration + } + + fun registerUnpackageTask( + project: Project, + configuration: Configuration + ): TaskProvider { + check(configuration.name.startsWith(ConfigurationNamePrefix)) { "Invalid configuration: ${configuration.name}" } + check(configuration.name.endsWith(ConfigurationNameSuffix)) { "Invalid configuration: ${configuration.name}" } + val targetName = configuration.name.removeSurrounding(ConfigurationNamePrefix, ConfigurationNameSuffix) + return project.tasks.register("unpackageKnee${targetName}Codegen") { + codegenFiles.from(configuration.incoming.artifactView { + attributes.attribute(ArtifactTypeAttribute, DirArtifactType) + }.files) + } + } + +} \ No newline at end of file diff --git a/knee-gradle-plugin/src/main/kotlin/KneePlugin.kt b/knee-gradle-plugin/src/main/kotlin/KneePlugin.kt new file mode 100644 index 0000000..0eee076 --- /dev/null +++ b/knee-gradle-plugin/src/main/kotlin/KneePlugin.kt @@ -0,0 +1,185 @@ +package io.deepmedia.tools.knee.plugin.gradle + +import com.android.build.api.dsl.CommonExtension +import io.deepmedia.tools.knee.plugin.gradle.utils.* +import io.deepmedia.tools.knee.plugin.gradle.utils.androidAbi +import io.deepmedia.tools.knee.plugin.gradle.utils.configureValidSourceSets +import io.deepmedia.tools.knee.plugin.gradle.utils.isValidBackend +import io.deepmedia.tools.knee.plugin.gradle.utils.isValidFrontend +import org.gradle.api.Project +import org.gradle.api.file.Directory +import org.gradle.api.provider.Provider +import org.gradle.kotlin.dsl.get +import org.gradle.kotlin.dsl.getByName +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.dsl.kotlinExtension +import org.jetbrains.kotlin.gradle.plugin.* +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget +import org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType +import org.jetbrains.kotlin.konan.target.KonanTarget + +@Suppress("unused") +class KneePlugin : KotlinCompilerPluginSupportPlugin { + + override fun apply(target: Project) { + val knee = target.extensions.create("knee", KneeExtension::class.java) + knee.projectName = target.name + + applyDependencies(target, knee) + applyConnection(target, knee) + } + + private fun applyDependencies(target: Project, knee: KneeExtension) { + target.plugins.withId("org.jetbrains.kotlin.multiplatform") { + val kotlin = target.kotlinExtension as KotlinMultiplatformExtension + kotlin.configureValidSourceSets(isValid = { isValidBackend || isValidFrontend }, log = knee::log) { sourceSet -> + knee.log("Adding knee-runtime and knee-annotations dependency to $sourceSet...") + sourceSet.dependencies { + implementation("$KneeGroup:knee-runtime:$KneeVersion") + implementation("$KneeGroup:knee-annotations:$KneeVersion") + } + } + } + target.plugins.withId("org.jetbrains.kotlin.android") { + knee.log("Adding knee-runtime and knee-annotations dependency to main sourceSet...") + target.kotlinExtension.sourceSets["main"].dependencies { + implementation("$KneeGroup:knee-runtime:$KneeVersion") + implementation("$KneeGroup:knee-annotations:$KneeVersion") + } + } + } + + private fun applyConnection(target: Project, knee: KneeExtension) { + val connectionInfo = run { + val binaryDirectory = target.layout.buildDirectory.get().dir("knee").dir("bin") + mapOf( + NativeBuildType.DEBUG to binaryDirectory.dir("debug"), + NativeBuildType.RELEASE to binaryDirectory.dir("release"), + ) + } + connectionInfo.forEach { (build, directory) -> + prepareConnection(target, knee, build, directory) + } + target.plugins.withId("org.jetbrains.kotlin.multiplatform") { + val kotlin = target.kotlinExtension as KotlinMultiplatformExtension + target.afterEvaluate { + if (knee.connectTargets.get()) { + val backends = kotlin.targets.matching { it.isValidBackend }.toList() + val frontends = kotlin.targets.matching { it.isValidFrontend }.toList() + if (backends.isEmpty() || frontends.isEmpty()) return@afterEvaluate + connectionInfo.forEach { (build, directory) -> + @Suppress("UNCHECKED_CAST") + performConnection(target, knee, build, directory, backends as List) + } + } + } + } + } + + /** + * Starting from Kotlin 2.0.0/AGP-something, jniLibs.srcDir() doesn't seem to work if called in after evaluate. + * So we need to run this in the configuration stage, even if [KneeExtension.connectTargets] was/will be set to false! + */ + private fun prepareConnection( + target: Project, + knee: KneeExtension, + buildType: NativeBuildType, + directory: Directory + ) { + val androidBuildType = buildType.toString().lowercase() + target.configureAndroidExtension { + it.sourceSets { + getByName(androidBuildType) { + knee.log("[prepareConnection] [$buildType] android.sourceSets.$androidBuildType.jniLibsSrc = $directory") + jniLibs.srcDir(directory) + } + } + } + } + + @Suppress("DefaultLocale") + private fun performConnection( + target: Project, + knee: KneeExtension, + buildType: NativeBuildType, + binaryDirectory: Directory, + backends: List + ) { + knee.log("[performConnection] [$buildType] backends=${backends.map { it.name }}") + // 1. add debug and release shared libraries for all backend targets + val binaries = backends.map { backend -> + val backendBinaryDirectory = binaryDirectory.dir(backend.androidAbi) + val lib = with(backend.binaries) { + knee.log("[performConnection/1] [$buildType] [backend:${backend.name}] creating sharedLib") + findSharedLib(buildType) ?: run { + sharedLib(listOf(buildType)) + getSharedLib(buildType) + } + } + // 2. generate shared libraries in a folder that AGP likes + // lib.outputDirectory is a var but it sometimes fails to change the linkTask which is what matters + // depends on order of instantiations. Do both, just in case someone depends on lib.outputDirectory + lib.outputDirectory = backendBinaryDirectory.asFile + knee.log("[performConnection/2] [$buildType] [backend:${backend.name}] configured sharedLib ${lib.name} (${lib.outputDirectory})") + lib.linkTaskProvider.configure { destinationDirectory.set(backendBinaryDirectory) } + lib + } + + // 3. link AGP tasks + // One option is the mergeJniLibFolders, but that only models one of the dependencies (.so files) + // and not the other (.kt files), so we resort to preBuild. + // TODO: check if there's a way to add the dependency implicitly in jniLibs.srcDir and java.srcDir + // TODO: if there isn't, model dependencies separately. There's no reason why something like compileDebugKotlinAndroid + // should LINK the binaries. It should only compile the source code. + val androidBuildType = buildType.toString().lowercase() + target.tasks.named("pre${androidBuildType.capitalize()}Build").configure { + knee.log("[performConnection/3] [$buildType] task $name now depends on binary tasks ${binaries.map { it.linkTaskName }}") + dependsOn(*binaries.map { it.linkTaskName }.toTypedArray()) + } + + // 4. pass the bin folder and the source code folder to AGP + target.configureAndroidExtension { + it.sourceSets { + named("main").configure { + // We generate source code for all backends, but we can only use one - let's take the first + // Note: we do this twice, one per build type. Should prob do it once. + // Note: intentionally using 'java', 'kotlin' would not work for some reason. + knee.log("[performConnection/4] [$buildType] android.sourceSets.main.javaSrc = ${knee.generatedSourceDirectory(backends.first()).get()}") + java.srcDir(knee.generatedSourceDirectory(backends.first()).get()) + } + } + } + } + + override fun getCompilerPluginId() = "knee-compiler-plugin" + + override fun getPluginArtifact() = SubpluginArtifact( + groupId = KneeGroup, + artifactId = getCompilerPluginId(), + version = KneeVersion + ) + + override fun isApplicable(kotlinCompilation: KotlinCompilation<*>): Boolean { + return kotlinCompilation.target.isValidBackend + && kotlinCompilation.compilationName == KotlinCompilation.MAIN_COMPILATION_NAME + } + + override fun applyToCompilation(kotlinCompilation: KotlinCompilation<*>): Provider> { + val project = kotlinCompilation.target.project + val knee = project.extensions.getByType(KneeExtension::class.java) + + // Compute the output directory using the target name + // We can ignore compilation name because of isApplicable - we only apply on 'main' + val outputDir = knee.generatedSourceDirectory(kotlinCompilation.target as KotlinNativeTarget) + kotlinCompilation.compileTaskProvider.get().outputs.dir(outputDir) + return project.provider { + listOf( + SubpluginOption(key = "enabled", value = knee.enabled.get().toString()), + SubpluginOption(key = "verboseLogs", value = knee.verboseLogs.get().toString()), + SubpluginOption(key = "verboseRuntime", value = knee.verboseRuntime.get().toString()), + SubpluginOption(key = "verboseSources", value = knee.verboseSources.get().toString()), + SubpluginOption(key = "outputDir", value = outputDir.get().asFile.absolutePath), + ) + } + } +} diff --git a/knee-gradle-plugin/src/main/kotlin/tasks/UnpackageCodegenSources.kt b/knee-gradle-plugin/src/main/kotlin/tasks/UnpackageCodegenSources.kt new file mode 100644 index 0000000..efdb7cf --- /dev/null +++ b/knee-gradle-plugin/src/main/kotlin/tasks/UnpackageCodegenSources.kt @@ -0,0 +1,25 @@ +package tasks + +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.ProjectLayout +import org.gradle.api.model.ObjectFactory +import org.gradle.api.tasks.Copy +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.OutputDirectory +import javax.inject.Inject + +open class UnpackageCodegenSources @Inject constructor(objects: ObjectFactory, layout: ProjectLayout) : Copy() { + @get:InputFiles + val codegenFiles: ConfigurableFileCollection = objects.fileCollection() + + @get:OutputDirectory + val outputDir: DirectoryProperty = objects.directoryProperty() + .convention(layout.buildDirectory.dir("codegen")) + + init { + from(codegenFiles) + into(outputDir) + exclude { it.name == "META-INF" } + } +} \ No newline at end of file diff --git a/knee-gradle-plugin/src/main/kotlin/utils/AndroidUtils.kt b/knee-gradle-plugin/src/main/kotlin/utils/AndroidUtils.kt new file mode 100644 index 0000000..15bd26e --- /dev/null +++ b/knee-gradle-plugin/src/main/kotlin/utils/AndroidUtils.kt @@ -0,0 +1,25 @@ +package io.deepmedia.tools.knee.plugin.gradle.utils + +import com.android.build.api.dsl.CommonExtension +import org.gradle.api.Project +import org.gradle.kotlin.dsl.getByName +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget +import org.jetbrains.kotlin.konan.target.KonanTarget + + +internal val KotlinNativeTarget.androidAbi get() = when (konanTarget) { + is KonanTarget.ANDROID_ARM32 -> "armeabi-v7a" + is KonanTarget.ANDROID_ARM64 -> "arm64-v8a" + is KonanTarget.ANDROID_X64 -> "x86_64" + is KonanTarget.ANDROID_X86 -> "x86" + else -> error("Unknown KonanTarget $konanTarget") +} + +internal fun Project.configureAndroidExtension(block: (CommonExtension<*, *, *, *, *>) -> Unit) { + fun runBlock() { + val android = extensions.getByName>("android") + block(android) + } + plugins.withId("com.android.application") { runBlock() } + plugins.withId("com.android.library") { runBlock() } +} \ No newline at end of file diff --git a/knee-gradle-plugin/src/main/kotlin/utils/SourceSetUtils.kt b/knee-gradle-plugin/src/main/kotlin/utils/SourceSetUtils.kt new file mode 100644 index 0000000..e55f016 --- /dev/null +++ b/knee-gradle-plugin/src/main/kotlin/utils/SourceSetUtils.kt @@ -0,0 +1,97 @@ +package io.deepmedia.tools.knee.plugin.gradle.utils + +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet +import org.jetbrains.kotlin.gradle.plugin.KotlinTarget +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget +import org.jetbrains.kotlin.konan.target.Family + +// "afterEvaluate" does nothing when the project is already in executed state +/* private fun Project.whenEvaluated(fn: Project.() -> T) { + if (state.executed) fn() + else afterEvaluate { fn() } +} */ + +internal val KotlinTarget.isValidBackend: Boolean + get() = platformType == KotlinPlatformType.native + && (this as KotlinNativeTarget).konanTarget.family == Family.ANDROID + +internal val KotlinTarget.isValidFrontend: Boolean + get() = platformType == KotlinPlatformType.androidJvm + +/** + * Adding the runtime dependency to multiplatform projects it's tricky because + * 1. We don't really know the target hierarchy + * 2. Kotlin plugin is not smart enough to commonize the dependency in some edge cases + * For example, if: + * - we add the dep individually to all four androidNative* targets + * - project has all androidNative* targets plus ios targets + * ... then in the project androidNativeMain, Knee won't resolve. + * + * I think in the long run this will be fixed by Kotlin, but for now we use afterEvaluate + * and the same logic that AtomicFU uses: + * https://github.com/Kotlin/kotlinx-atomicfu/blob/0.22.0/atomicfu-gradle-plugin/src/main/kotlin/kotlinx/atomicfu/plugin/gradle/AtomicFUGradlePlugin.kt#L375 + * + * Still there are a couple of issues + * 1. It takes a snapshot of the targets in afterEvaluate. Other targets might be added later + * 2. It is redundant. We'll add the dependency to all source sets that share valid targets, like + * androidNativeX86Main, androidNativeX86Test, androidNativeMain, androidNativeTest, ... + */ +internal fun KotlinMultiplatformExtension.configureValidSourceSets( + isValid: KotlinTarget.() -> Boolean, + log: (String) -> Unit, + block: (KotlinSourceSet) -> Unit +) { + targets.configureEach { + if (isValid()) { + log("configureValidSourceSets: $targetName is valid, accepting all compilations...") + compilations.configureEach { defaultSourceSet(block) } + } else if (platformType != KotlinPlatformType.common) { + log("configureValidSourceSets: $targetName is invalid, dropping all compilations...") + } else { + // Intermediate source sets are added as compilations to the metadata/common target. + // Investigate them, assuming that by the time the compilation is created, all targets are already known + // This might not be true + compilations.configureEach { + log("configureValidSourceSets: investigating common:$compilationName...") + val associatedTargets = this@configureValidSourceSets.targets.matching { + if (it.platformType == KotlinPlatformType.common) return@matching false + val descendantSets = it.compilations.flatMap { it.allKotlinSourceSets } + descendantSets.intersect(kotlinSourceSets).isNotEmpty() + }.toList() + if (associatedTargets.isNotEmpty() && associatedTargets.all(isValid)) { + log("configureValidSourceSets: common:$compilationName accepted ($associatedTargets)") + defaultSourceSet(block) + } else { + log("configureValidSourceSets: common:$compilationName rejected ($associatedTargets)") + } + } + } + } + + // Old impl + /* this.project.whenEvaluated { + val sourceSets = hashMapOf>>() + targets.flatMap { it.compilations }.forEach { compilation -> + compilation.allKotlinSourceSets.forEach { sourceSet -> + sourceSets.getOrPut(sourceSet) { mutableListOf() }.add(compilation) + } + log("configureValidSourceSets: Analyzing ${compilation.target.targetName}::${compilation::compilationName}: sets = ${compilation.allKotlinSourceSets.map { it.name }}") + } + val matches = sourceSets.filter { (set, compilations) -> + val valid = compilations + .filter { it.platformType != KotlinPlatformType.common } + .all { it.target.isValid() } + if (valid) { + log("configureValidSourceSets: ${set.name} ACCEPTED (all non-common compilations belong to a valid target)") + } else { + log("configureValidSourceSets: ${set.name} REJECTED (some non-common compilations belong to invalid targets)") + } + valid + } + matches.forEach { + block(it.key) + } + } */ +} \ No newline at end of file diff --git a/knee-runtime/.gitignore b/knee-runtime/.gitignore new file mode 100644 index 0000000..f3d6549 --- /dev/null +++ b/knee-runtime/.gitignore @@ -0,0 +1 @@ +/build/ \ No newline at end of file diff --git a/knee-runtime/build.gradle.kts b/knee-runtime/build.gradle.kts new file mode 100644 index 0000000..246afb0 --- /dev/null +++ b/knee-runtime/build.gradle.kts @@ -0,0 +1,82 @@ +import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget + +plugins { + kotlin("multiplatform") + id("io.deepmedia.tools.deployer") +} + +kotlin { + applyDefaultHierarchyTemplate { + common { + group("backend") { + // withNative() + group("androidNative") { + withAndroidNative() + } + group("prebuiltHeaders") { + withMacos() + withMingwX64() + withLinuxX64() + } + } + } + } + + // https://github.com/androidx/androidx/blob/0d0dcddc46e8267a2f7546f40fc6c2fbb1516c3d/buildSrc/private/src/main/kotlin/androidx/build/clang/NativeTargetCompilation.kt#L91 + // https://github.com/jonnyzzz/kotlin-jni-mix/blob/94ca9a01efec003d35fea96a3de87c517b88e5be/build.gradle.kts#L37 + // https://github.com/mpetuska/fake-kamera/commit/d5a2e5bcef4c754fc3f6b231e9ea318e826ce56a + // https://github.com/DatL4g/Sekret/blob/f1f3d30bf5b1ffed1b518a908cdd8515b078cdc3/sekret-lib/build.gradle.kts#L41 + fun KotlinNativeTarget.configurePrebuiltCinterop(folder: String, subfolder: String) { + val file = rootProject.layout.projectDirectory.dir("dependencies").dir("jdk17").dir(folder).dir("include") + val defFiles = layout.projectDirectory.dir("src").dir("prebuiltHeadersMain").dir("interop") + compilations[KotlinCompilation.MAIN_COMPILATION_NAME].cinterops { + create("jni") { + definitionFile.set(defFiles.file("jni_prebuilt.def")) + packageName("io.deepmedia.tools.knee.runtime.internal") + includeDirs(file, file.dir(subfolder)) + } + } + } + + + // backend + androidNativeArm32() // { configureAndroidCInterop() } + androidNativeArm64() // { configureAndroidCInterop() } + androidNativeX64() // { configureAndroidCInterop() } + androidNativeX86() // { configureAndroidCInterop() } + // linuxX64 { configurePrebuiltCinterop("linux-x86", "linux") } + // mingwX64 { configurePrebuiltCinterop("windows-x86", "win32") } + // macosArm64 { configurePrebuiltCinterop("darwin-arm64", "darwin") } + // macosX64 { configurePrebuiltCinterop("darwin-x86", "darwin") } + + // frontend + jvmToolchain(11) + jvm(name = "frontend") + + + sourceSets.configureEach { + languageSettings { + optIn("kotlin.ExperimentalUnsignedTypes") + optIn("kotlinx.cinterop.UnsafeNumber") + optIn("kotlinx.cinterop.ExperimentalForeignApi") + optIn("kotlin.experimental.ExperimentalNativeApi") + } + } + + sourceSets.commonMain.configure { + dependencies { + api(project(":knee-annotations")) + api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1") + } + } +} + +deployer { + content.kotlinComponents { + emptyDocs() + } +} + +// Solves a problem with includeBuild() in the tests module +val runCommonizer by tasks.registering { } diff --git a/knee-runtime/src/androidNativeMain/kotlin/JniDefinitions.android.kt b/knee-runtime/src/androidNativeMain/kotlin/JniDefinitions.android.kt new file mode 100644 index 0000000..5a79bac --- /dev/null +++ b/knee-runtime/src/androidNativeMain/kotlin/JniDefinitions.android.kt @@ -0,0 +1,7 @@ +package io.deepmedia.tools.knee.runtime + +// actual val JNI_OK: Int get() = platform.android.JNI_OK +// actual val JNI_VERSION_1_6: Int get() = platform.android.JNI_VERSION_1_6 +// actual typealias JNIInvokeInterface = platform.android.JNIInvokeInterface +// actual typealias JNINativeInterface = platform.android.JNINativeInterface + diff --git a/knee-runtime/src/backendMain/kotlin/Init.kt b/knee-runtime/src/backendMain/kotlin/Init.kt new file mode 100644 index 0000000..acffac4 --- /dev/null +++ b/knee-runtime/src/backendMain/kotlin/Init.kt @@ -0,0 +1,38 @@ +package io.deepmedia.tools.knee.runtime + +import io.deepmedia.tools.knee.runtime.compiler.* +import io.deepmedia.tools.knee.runtime.compiler.initBoxMethods +import io.deepmedia.tools.knee.runtime.compiler.initBuffers +import io.deepmedia.tools.knee.runtime.compiler.initExceptions +import io.deepmedia.tools.knee.runtime.compiler.initInstances +import io.deepmedia.tools.knee.runtime.compiler.initSuspend +import io.deepmedia.tools.knee.runtime.module.KneeModule +import kotlinx.atomicfu.atomic + +private val kneeInitialized = atomic(0L) + +internal var initializationData: InitializationData? = null + +internal class InitializationData( + val jvm: JavaVirtualMachine, + val exceptions: Set +) + +fun initKnee(environment: JniEnvironment, vararg modules: KneeModule) { + val vm = environment.javaVM + val id = vm.rawValue.toLong() + val oldId = kneeInitialized.getAndSet(id) + if (id != oldId) { + val exceptions = mutableSetOf() + modules.forEach { it.collectExceptions(exceptions) } + initializationData = InitializationData(vm, exceptions) + initSuspend(environment) + initInstances(environment) + initBoxMethods(environment) + initExceptions(environment) + initBuffers(environment) + } + modules.forEach { + it.initializeIfNeeded(environment) + } +} \ No newline at end of file diff --git a/knee-runtime/src/backendMain/kotlin/JniApi.kt b/knee-runtime/src/backendMain/kotlin/JniApi.kt new file mode 100644 index 0000000..947b501 --- /dev/null +++ b/knee-runtime/src/backendMain/kotlin/JniApi.kt @@ -0,0 +1,406 @@ +@file:Suppress("unused") + +package io.deepmedia.tools.knee.runtime + +import io.deepmedia.tools.knee.runtime.compiler.processJvmException +import kotlinx.cinterop.* +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract +import kotlin.Int +import platform.android.* + +// JNI APIs, with same namings and so on, just wrapped in a more convenient +// fashion, as extensions to CPointer<*>. + +typealias JniEnvironment = CPointer + +@PublishedApi +internal val JniEnvironment.api get() = pointed.pointed!! + +val JniEnvironment.javaVM: JavaVirtualMachine get() = memScoped { + val jvmPointer: CPointerVar = allocPointerTo() + val res = api.GetJavaVM!!(this@javaVM, jvmPointer.ptr) + check(res == JNI_OK) { "GetJavaVM failed: $res" } + jvmPointer.pointed!!.ptr +} + +fun JniEnvironment.findClass(name: String): jclass = memScoped { + // println("findClass=$name") + // try { + val jclass = api.FindClass!!(this@findClass, name.replace('.', '/').cstr.ptr) + checkNotNull(jclass) { + checkPendingJvmException() + "FindClass failed: class $name not found." + } + /* } catch (e: Throwable) { + println("findClass=$name FAILED=$e") + throw e + } finally { + println("findClass=$name END") + } */ +} + +data class JniNativeMethod( + val name: String, + val signature: String, + val pointer: COpaquePointer +) + +fun JniEnvironment.registerNatives( + classFqn: String, + vararg bindings: JniNativeMethod +): Unit = registerNatives(jclass = findClass(classFqn), bindings = bindings) + +fun JniEnvironment.registerNatives( + jclass: jclass, + vararg bindings: JniNativeMethod +): Unit = memScoped { + val jniMethods = allocArray(bindings.size) + bindings.forEachIndexed { index, binding -> + jniMethods[index].fnPtr = binding.pointer + jniMethods[index].name = binding.name.cstr.ptr + jniMethods[index].signature = binding.signature.cstr.ptr + } + val result = api.RegisterNatives!!(this@registerNatives, jclass, jniMethods, bindings.size) + check(result == 0) { "RegisterNatives failed: $result" } +} + +fun JniEnvironment.getStringUTFChars( + jstring: jstring +): CPointer = memScoped { + val result = api.GetStringUTFChars!!(this@getStringUTFChars, jstring, null) + checkNotNull(result) { "GetStringUTFChars failed." } + result +} + +fun JniEnvironment.releaseStringUTFChars( + jstring: jstring, + chars: CPointer +): Unit = memScoped { + api.ReleaseStringUTFChars!!(this@releaseStringUTFChars, jstring, chars) +} + +fun JniEnvironment.newStringUTF(string: String) = memScoped { + newStringUTF(utfChars = string.utf8.ptr) +} + +fun JniEnvironment.newStringUTF( + utfChars: CPointer, // null terminated +): jstring = memScoped { + val res = api.NewStringUTF!!(this@newStringUTF, utfChars) + checkNotNull(res) { "newStringUTF failed." } + return res +} + +fun JniEnvironment.getArrayLength(array: jarray): Int { + return api.GetArrayLength!!(this, array) +} + +// Ignoring copy flag... +fun JniEnvironment.getPrimitiveArrayCritical(array: jarray): COpaquePointer { + return checkNotNull(api.GetPrimitiveArrayCritical!!(this, array, null)) { + "GetPrimitiveArrayCritical failed." + } +} + +fun JniEnvironment.releasePrimitiveArrayCritical(array: jarray, handle: COpaquePointer, mode: kotlin.Int) { + api.ReleasePrimitiveArrayCritical!!(this, array, handle, mode) +} + +@OptIn(ExperimentalContracts::class) +inline fun JniEnvironment.usePrimitiveArrayCritical(array: jarray, mode: kotlin.Int, block: (COpaquePointer) -> R): R { + contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } + val handle = getPrimitiveArrayCritical(array) + return try { block(handle) } finally { + releasePrimitiveArrayCritical(array, handle, mode) + } +} + +fun JniEnvironment.getObjectArrayElement(array: jobjectArray, index: Int): jobject { + return checkNotNull(api.GetObjectArrayElement!!(this, array, index)) { "GetObjectArrayElement failed." } +} + +fun JniEnvironment.setObjectArrayElement(array: jobjectArray, index: Int, value: jobject) { + api.SetObjectArrayElement!!(this, array, index, value) +} + +fun JniEnvironment.newByteArray(size: Int): jbyteArray { + return checkNotNull(api.NewByteArray!!(this, size)) { "NewByteArray failed." } +} + +fun JniEnvironment.newIntArray(size: Int): jintArray { + return checkNotNull(api.NewIntArray!!(this, size)) { "NewIntArray failed." } +} + +fun JniEnvironment.newLongArray(size: Int): jlongArray { + return checkNotNull(api.NewLongArray!!(this, size)) { "NewLongArray failed." } +} + +fun JniEnvironment.newFloatArray(size: Int): jfloatArray { + return checkNotNull(api.NewFloatArray!!(this, size)) { "NewFloatArray failed." } +} + +fun JniEnvironment.newDoubleArray(size: Int): jdoubleArray { + return checkNotNull(api.NewDoubleArray!!(this, size)) { "NewDoubleArray failed." } +} + +fun JniEnvironment.newBooleanArray(size: Int): jbooleanArray { + return checkNotNull(api.NewBooleanArray!!(this, size)) { "NewBooleanArray failed." } +} + +fun JniEnvironment.newObjectArray(size: Int, klass: jclass, initial: jobject? = null): jobjectArray { + return checkNotNull(api.NewObjectArray!!(this, size, klass, initial)) { "NewObjectArray failed." } +} + +@Suppress("UNCHECKED_CAST") +fun JniEnvironment.newObject(klass: jclass, constructor: jmethodID, vararg args: Any?): jobject { + if (args.isEmpty()) { + val func = api.NewObject!! as CPointer jobject?>> + func(this, klass, constructor) + } else { + memScoped { + val array = allocArray(args.size) + args.forEachIndexed { index, arg -> arg.jvalueOrThrow(array[index]) } + api.NewObjectA!!(this@newObject, klass, constructor, array) + } + }.let { + return checkNotNull(it) { "NewObject failed." } + } +} + +fun JniEnvironment.newDirectByteBuffer(address: COpaquePointer, capacity: Long): jobject { + return checkNotNull(api.NewDirectByteBuffer!!(this, address, capacity)) { "newDirectByteBuffer failed!" } +} + +fun JniEnvironment.getDirectBufferAddress(buffer: jobject): COpaquePointer { + return checkNotNull(api.GetDirectBufferAddress!!(this, buffer)) { "GetDirectBufferAddress failed!" } +} + +fun JniEnvironment.getDirectBufferCapacity(buffer: jobject): Long { + return checkNotNull(api.GetDirectBufferCapacity!!(this, buffer)) { "GetDirectBufferCapacity failed!" } +} + +fun JniEnvironment.setByteArrayRegion(array: jbyteArray, start: Int, length: Int, data: CPointer) { + api.SetByteArrayRegion!!(this, array, start, length, data) +} + +fun JniEnvironment.setIntArrayRegion(array: jintArray, start: Int, length: Int, data: CPointer) { + api.SetIntArrayRegion!!(this, array, start, length, data) +} + +fun JniEnvironment.setLongArrayRegion(array: jlongArray, start: Int, length: Int, data: CPointer) { + api.SetLongArrayRegion!!(this, array, start, length, data) +} + +fun JniEnvironment.setFloatArrayRegion(array: jfloatArray, start: Int, length: Int, data: CPointer) { + api.SetFloatArrayRegion!!(this, array, start, length, data) +} + +fun JniEnvironment.setDoubleArrayRegion(array: jdoubleArray, start: Int, length: Int, data: CPointer) { + api.SetDoubleArrayRegion!!(this, array, start, length, data) +} + +fun JniEnvironment.setBooleanArrayRegion(array: jbooleanArray, start: Int, length: Int, data: CPointer) { + api.SetBooleanArrayRegion!!(this, array, start, length, data) +} + +fun JniEnvironment.newGlobalRef(jobject: jobject): jobject { + return checkNotNull(api.NewGlobalRef!!(this, jobject)) { + "NewGlobalRef failed, out of memory?" + } +} + +fun JniEnvironment.deleteLocalRef(jobject: jobject) { + api.DeleteLocalRef!!(this, jobject) +} + +fun JniEnvironment.deleteGlobalRef(jobject: jobject) { + api.DeleteGlobalRef!!(this, jobject) +} + +fun JniEnvironment.isSameObject(first: jobject, second: jobject): Boolean { + return api.IsSameObject!!(this, first, second).toInt() == JNI_TRUE +} + +fun JniEnvironment.getObjectClass(jobject: jobject): jclass { + return checkNotNull(api.GetObjectClass!!(this, jobject)) { + "getObjectClass failed" + } +} + +fun JniEnvironment.getMethodId(jclass: jclass, name: String, signature: String): jmethodID = memScoped { + checkNotNull(api.GetMethodID!!(this@getMethodId, jclass, name.cstr.ptr, signature.cstr.ptr)) { + // Checking for null is enough to understand if this failed, but there might still be an exception at the JVM level + // like NoSuchMethodError. Throw that if present, so future JNI calls will not fail because it will be cleared. + checkPendingJvmException() + "getMethodId failed ($jclass, $name, $signature)" + } +} + +fun JniEnvironment.getStaticMethodId(jclass: jclass, name: String, signature: String): jmethodID = memScoped { + checkNotNull(api.GetStaticMethodID!!(this@getStaticMethodId, jclass, name.cstr.ptr, signature.cstr.ptr)) { + // Checking for null is enough to understand if this failed, but there might still be an exception at the JVM level + // like NoSuchMethodError. Throw that if present, so future JNI calls will not fail because it will be cleared. + checkPendingJvmException() + "getStaticMethodId failed ($jclass, $name, $signature)" + } +} + +fun JniEnvironment.getFieldId(jclass: jclass, name: String, signature: String): jfieldID = memScoped { + checkNotNull(api.GetFieldID!!(this@getFieldId, jclass, name.cstr.ptr, signature.cstr.ptr)) { + // Checking for null is enough to understand if this failed, but there might still be an exception at the JVM level + // like NoSuchMethodError. Throw that if present, so future JNI calls will not fail because it will be cleared. + checkPendingJvmException() + "getMethodId failed ($jclass, $name, $signature)" + } +} + +fun JniEnvironment.getStaticFieldId(jclass: jclass, name: String, signature: String): jfieldID = memScoped { + checkNotNull(api.GetStaticFieldID!!(this@getStaticFieldId, jclass, name.cstr.ptr, signature.cstr.ptr)) { + // Checking for null is enough to understand if this failed, but there might still be an exception at the JVM level + // like NoSuchMethodError. Throw that if present, so future JNI calls will not fail because it will be cleared. + checkPendingJvmException() + "getMethodId failed ($jclass, $name, $signature)" + } +} + +@PublishedApi +internal fun Any?.jvalueOrThrow(value: jvalue) { + when (this) { + null -> value.l = null + is CPointer<*> -> value.l = this + is jshort -> value.s = this + is jchar -> value.c = this + is jdouble -> value.d = this + is jbyte -> value.b = this + is jfloat -> value.f = this + is jint -> value.i = this + is jlong -> value.j = this + is jboolean -> value.z = this + else -> error("Unsupported argument: $this") + } +} + +/** + * Note: [noArgsInvoke] and [arrayArgsInvoke] are a workaround for https://youtrack.jetbrains.com/issue/KT-55776 + * The function invocation must happen on the parent function otherwise compiler throws the error above. + */ +@PublishedApi +internal inline fun JniEnvironment.callMethod( + jobjectOrJClass: COpaquePointer, + method: jmethodID, + noArgsFun: JNINativeInterface.() -> COpaquePointer, + arrayArgsFun: JNINativeInterface.() -> CPointer?) -> ReturnType>>, + noArgsInvoke: CPointer ReturnType>>.(JniEnvironment, COpaquePointer, jmethodID) -> ReturnType, + arrayArgsInvoke: CPointer?) -> ReturnType>>.(JniEnvironment, COpaquePointer, jmethodID, CPointer?) -> ReturnType, + vararg args: Any?, +): ReturnType { + return if (args.isEmpty()) { + @Suppress("UNCHECKED_CAST") + val func = api.noArgsFun() as CPointer ReturnType>> + func.noArgsInvoke(this, jobjectOrJClass, method) + } else { + memScoped { + val array = allocArray(args.size) + args.forEachIndexed { index, arg -> arg.jvalueOrThrow(array[index]) } + api.arrayArgsFun().arrayArgsInvoke(this@callMethod, jobjectOrJClass, method, array) + } + }.also { + checkPendingJvmException() + } +} + +fun JniEnvironment.callObjectMethod(jobject: jobject, method: jmethodID, vararg args: Any?): jobject? { + return callMethod(jobject, method, { CallObjectMethod!! }, { CallObjectMethodA!! }, { a,b,c->this(a,b,c) }, { a,b,c,d->this(a,b,c,d) }, *args) +} + +fun JniEnvironment.callBooleanMethod(jobject: jobject, method: jmethodID, vararg args: Any?): jboolean { + return callMethod(jobject, method, { CallBooleanMethod!! }, { CallBooleanMethodA!! }, { a,b,c->this(a,b,c) }, { a,b,c,d->this(a,b,c,d) }, *args) +} + +fun JniEnvironment.callByteMethod(jobject: jobject, method: jmethodID, vararg args: Any?): Byte { + return callMethod(jobject, method, { CallByteMethod!! }, { CallByteMethodA!! }, { a,b,c->this(a,b,c) }, { a,b,c,d->this(a,b,c,d) }, *args) +} + +fun JniEnvironment.callCharMethod(jobject: jobject, method: jmethodID, vararg args: Any?): jchar { + return callMethod(jobject, method, { CallCharMethod!! }, { CallCharMethodA!! }, { a,b,c->this(a,b,c) }, { a,b,c,d->this(a,b,c,d) }, *args) +} + +fun JniEnvironment.callShortMethod(jobject: jobject, method: jmethodID, vararg args: Any?): Short { + return callMethod(jobject, method, { CallShortMethod!! }, { CallShortMethodA!! }, { a,b,c->this(a,b,c) }, { a,b,c,d->this(a,b,c,d) }, *args) +} + +fun JniEnvironment.callIntMethod(jobject: jobject, method: jmethodID, vararg args: Any?): Int { + return callMethod(jobject, method, { CallIntMethod!! }, { CallIntMethodA!! }, { a,b,c->this(a,b,c) }, { a,b,c,d->this(a,b,c,d) }, *args) +} + +fun JniEnvironment.callLongMethod(jobject: jobject, method: jmethodID, vararg args: Any?): Long { + return callMethod(jobject, method, { CallLongMethod!! }, { CallLongMethodA!! }, { a,b,c->this(a,b,c) }, { a,b,c,d->this(a,b,c,d) }, *args) +} + +fun JniEnvironment.callFloatMethod(jobject: jobject, method: jmethodID, vararg args: Any?): Float { + return callMethod(jobject, method, { CallFloatMethod!! }, { CallFloatMethodA!! }, { a,b,c->this(a,b,c) }, { a,b,c,d->this(a,b,c,d) }, *args) +} + +fun JniEnvironment.callDoubleMethod(jobject: jobject, method: jmethodID, vararg args: Any?): Double { + return callMethod(jobject, method, { CallDoubleMethod!! }, { CallDoubleMethodA!! }, { a,b,c->this(a,b,c) }, { a,b,c,d->this(a,b,c,d) }, *args) +} + +fun JniEnvironment.callVoidMethod(jobject: jobject, method: jmethodID, vararg args: Any?) { + return callMethod(jobject, method, { CallVoidMethod!! }, { CallVoidMethodA!! }, { a,b,c->this(a,b,c) }, { a,b,c,d->this(a,b,c,d) }, *args) +} + +fun JniEnvironment.callStaticObjectMethod(jclass: jclass, method: jmethodID, vararg args: Any?): jobject? { + return callMethod(jclass, method, { CallStaticObjectMethod!! }, { CallStaticObjectMethodA!! }, { a,b,c->this(a,b,c) }, { a,b,c,d->this(a,b,c,d) }, *args) +} + +fun JniEnvironment.callStaticBooleanMethod(jclass: jclass, method: jmethodID, vararg args: Any?): jboolean { + return callMethod(jclass, method, { CallStaticBooleanMethod!! }, { CallStaticBooleanMethodA!! }, { a,b,c->this(a,b,c) }, { a,b,c,d->this(a,b,c,d) }, *args) +} + +fun JniEnvironment.callStaticByteMethod(jclass: jclass, method: jmethodID, vararg args: Any?): Byte { + return callMethod(jclass, method, { CallStaticByteMethod!! }, { CallStaticByteMethodA!! }, { a,b,c->this(a,b,c) }, { a,b,c,d->this(a,b,c,d) }, *args) +} + +fun JniEnvironment.callStaticCharMethod(jclass: jclass, method: jmethodID, vararg args: Any?): jchar { + return callMethod(jclass, method, { CallStaticCharMethod!! }, { CallStaticCharMethodA!! }, { a,b,c->this(a,b,c) }, { a,b,c,d->this(a,b,c,d) }, *args) +} + +fun JniEnvironment.callStaticShortMethod(jclass: jclass, method: jmethodID, vararg args: Any?): Short { + return callMethod(jclass, method, { CallStaticShortMethod!! }, { CallStaticShortMethodA!! }, { a,b,c->this(a,b,c) }, { a,b,c,d->this(a,b,c,d) }, *args) +} + +fun JniEnvironment.callStaticIntMethod(jclass: jclass, method: jmethodID, vararg args: Any?): Int { + return callMethod(jclass, method, { CallStaticIntMethod!! }, { CallStaticIntMethodA!! }, { a,b,c->this(a,b,c) }, { a,b,c,d->this(a,b,c,d) }, *args) +} + +fun JniEnvironment.callStaticLongMethod(jclass: jclass, method: jmethodID, vararg args: Any?): Long { + return callMethod(jclass, method, { CallStaticLongMethod!! }, { CallStaticLongMethodA!! }, { a,b,c->this(a,b,c) }, { a,b,c,d->this(a,b,c,d) }, *args) +} + +fun JniEnvironment.callStaticFloatMethod(jclass: jclass, method: jmethodID, vararg args: Any?): Float { + return callMethod(jclass, method, { CallStaticFloatMethod!! }, { CallStaticFloatMethodA!! }, { a,b,c->this(a,b,c) }, { a,b,c,d->this(a,b,c,d) }, *args) +} + +fun JniEnvironment.callStaticDoubleMethod(jclass: jclass, method: jmethodID, vararg args: Any?): Double { + return callMethod(jclass, method, { CallStaticDoubleMethod!! }, { CallStaticDoubleMethodA!! }, { a,b,c->this(a,b,c) }, { a,b,c,d->this(a,b,c,d) }, *args) +} + +fun JniEnvironment.callStaticVoidMethod(jclass: jclass, method: jmethodID, vararg args: Any?) { + return callMethod(jclass, method, { CallStaticVoidMethod!! }, { CallStaticVoidMethodA!! }, { a,b,c->this(a,b,c) }, { a,b,c,d->this(a,b,c,d) }, *args) +} + +fun JniEnvironment.`throw`(throwable: jthrowable) { + val res = api.Throw!!(this, throwable) + check(res == 0) { "Throw failed: $res" } +} + +@PublishedApi // < because it's used in inline fun +internal fun JniEnvironment.checkPendingJvmException() { + if (api.ExceptionCheck!!(this) == platform.android.JNI_TRUE.toUByte()) { + val throwable = api.ExceptionOccurred!!(this) + api.ExceptionClear!!(this) + throw processJvmException(throwable) + } +} \ No newline at end of file diff --git a/knee-runtime/src/backendMain/kotlin/JniApi_Fields.kt b/knee-runtime/src/backendMain/kotlin/JniApi_Fields.kt new file mode 100644 index 0000000..ff7cc05 --- /dev/null +++ b/knee-runtime/src/backendMain/kotlin/JniApi_Fields.kt @@ -0,0 +1,82 @@ +package io.deepmedia.tools.knee.runtime + +import kotlinx.cinterop.cstr +import kotlinx.cinterop.invoke +import platform.android.jobject +import platform.android.jfieldID +import platform.android.jboolean +import platform.android.jchar + +// TODO: setters + +fun JniEnvironment.getIntField(jobject: jobject, field: jfieldID): Int { + return api.GetIntField!!(this, jobject, field).also { checkPendingJvmException() } +} + +fun JniEnvironment.getBooleanField(jobject: jobject, field: jfieldID): jboolean { + return api.GetBooleanField!!(this, jobject, field).also { checkPendingJvmException() } +} + +fun JniEnvironment.getLongField(jobject: jobject, field: jfieldID): Long { + return api.GetLongField!!(this, jobject, field).also { checkPendingJvmException() } +} + +fun JniEnvironment.getFloatField(jobject: jobject, field: jfieldID): Float { + return api.GetFloatField!!(this, jobject, field).also { checkPendingJvmException() } +} + +fun JniEnvironment.getDoubleField(jobject: jobject, field: jfieldID): Double { + return api.GetDoubleField!!(this, jobject, field).also { checkPendingJvmException() } +} + +fun JniEnvironment.getShortField(jobject: jobject, field: jfieldID): Short { + return api.GetShortField!!(this, jobject, field).also { checkPendingJvmException() } +} + +fun JniEnvironment.getByteField(jobject: jobject, field: jfieldID): Byte { + return api.GetByteField!!(this, jobject, field).also { checkPendingJvmException() } +} + +fun JniEnvironment.getCharField(jobject: jobject, field: jfieldID): jchar { + return api.GetCharField!!(this, jobject, field).also { checkPendingJvmException() } +} + +fun JniEnvironment.getObjectField(jobject: jobject, field: jfieldID): jobject? { + return api.GetObjectField!!(this, jobject, field).also { checkPendingJvmException() } +} + +fun JniEnvironment.getStaticIntField(jobject: jobject, field: jfieldID): Int { + return api.GetStaticIntField!!(this, jobject, field).also { checkPendingJvmException() } +} + +fun JniEnvironment.getStaticBooleanField(jobject: jobject, field: jfieldID): jboolean { + return api.GetStaticBooleanField!!(this, jobject, field).also { checkPendingJvmException() } +} + +fun JniEnvironment.getStaticLongField(jobject: jobject, field: jfieldID): Long { + return api.GetStaticLongField!!(this, jobject, field).also { checkPendingJvmException() } +} + +fun JniEnvironment.getStaticFloatField(jobject: jobject, field: jfieldID): Float { + return api.GetStaticFloatField!!(this, jobject, field).also { checkPendingJvmException() } +} + +fun JniEnvironment.getStaticDoubleField(jobject: jobject, field: jfieldID): Double { + return api.GetStaticDoubleField!!(this, jobject, field).also { checkPendingJvmException() } +} + +fun JniEnvironment.getStaticShortField(jobject: jobject, field: jfieldID): Short { + return api.GetStaticShortField!!(this, jobject, field).also { checkPendingJvmException() } +} + +fun JniEnvironment.getStaticByteField(jobject: jobject, field: jfieldID): Byte { + return api.GetStaticByteField!!(this, jobject, field).also { checkPendingJvmException() } +} + +fun JniEnvironment.getStaticCharField(jobject: jobject, field: jfieldID): jchar { + return api.GetStaticCharField!!(this, jobject, field).also { checkPendingJvmException() } +} + +fun JniEnvironment.getStaticObjectField(jobject: jobject, field: jfieldID): jobject? { + return api.GetStaticObjectField!!(this, jobject, field).also { checkPendingJvmException() } +} \ No newline at end of file diff --git a/knee-runtime/src/backendMain/kotlin/JniDefinitions.kt b/knee-runtime/src/backendMain/kotlin/JniDefinitions.kt new file mode 100644 index 0000000..9eac067 --- /dev/null +++ b/knee-runtime/src/backendMain/kotlin/JniDefinitions.kt @@ -0,0 +1,21 @@ +package io.deepmedia.tools.knee.runtime + +import kotlinx.cinterop.COpaquePointer +import kotlinx.cinterop.CPointed +import kotlinx.cinterop.CStructVar + +// typealias jobject = COpaquePointer +// typealias jclass = jobject +// typealias jstring = jobject +// expect val JNI_OK: Int +// expect val JNI_VERSION_1_6: Int +// expect class JNIInvokeInterface : CStructVar +// expect class JNINativeInterface : CStructVar +// typealias JavaVMVar = kotlinx.cinterop.CPointerVarOf +// typealias JavaVM = kotlinx.cinterop.CPointer +// typealias JNIEnvVar = kotlinx.cinterop.CPointerVarOf +// typealias JNIEnv = kotlinx.cinterop.CPointer + + + + diff --git a/knee-runtime/src/backendMain/kotlin/JvmApi.kt b/knee-runtime/src/backendMain/kotlin/JvmApi.kt new file mode 100644 index 0000000..61f6aec --- /dev/null +++ b/knee-runtime/src/backendMain/kotlin/JvmApi.kt @@ -0,0 +1,38 @@ +@file:Suppress("unused") + +package io.deepmedia.tools.knee.runtime + +import kotlinx.cinterop.* +import platform.android.* + +// JNI APIs, with same namings and so on, just wrapped in a more convenient +// fashion, as extensions to CPointer<*>. + +typealias JavaVirtualMachine = CPointer + +internal val JavaVirtualMachine.api get() = pointed.pointed!! + +/** + * Returns the [JniEnvironment] attached to the current thread. The environment is + * guaranteed to exist when calling this function from a JNI method, or more generally + * from JVM-managed threads that have an associated java.lang.Thread. + * In other cases, this function will return null - [attachCurrentThread] should be called instead. + */ +val JavaVirtualMachine.env: JniEnvironment? get() = memScoped { + val envPointer: CPointerVar = allocPointerTo() + val res = api.GetEnv!!(this@env, envPointer.ptr.reinterpret(), JNI_VERSION_1_6) + // NOTE: GetEnv returns JNI_EDETACHED if the current thread is not attached to the VM. + return if (res == JNI_OK) envPointer.pointed!!.ptr else null +} + +fun JavaVirtualMachine.attachCurrentThread(): JniEnvironment = memScoped { + val envPointer: CPointerVar = allocPointerTo() + val res = api.AttachCurrentThread!!(this@attachCurrentThread, envPointer.ptr.reinterpret(), null) + check(res == JNI_OK) { "AttachCurrentThread failed: $res" } + envPointer.pointed!!.ptr +} + +fun JavaVirtualMachine.detachCurrentThread() { + val res = api.DetachCurrentThread!!(this) + check(res == JNI_OK) { "DetachCurrentThread failed: $res" } +} diff --git a/knee-runtime/src/backendMain/kotlin/JvmHelpers.kt b/knee-runtime/src/backendMain/kotlin/JvmHelpers.kt new file mode 100644 index 0000000..aed5d25 --- /dev/null +++ b/knee-runtime/src/backendMain/kotlin/JvmHelpers.kt @@ -0,0 +1,28 @@ +@file:Suppress("unused") + +package io.deepmedia.tools.knee.runtime + + +val currentJavaVirtualMachine: JavaVirtualMachine + get() = checkNotNull(initializationData?.jvm) { "JVM is null. Did you forget to call initKnee?" } + + +inline fun JavaVirtualMachine.useEnv(block: (JniEnvironment) -> T): T { + var env = env + if (env != null) { + return block(env) + } + env = attachCurrentThread() + return try { + block(env) + } finally { + detachCurrentThread() + } +} + +// Just for compiler plugin, too lazy to use the property +// @PublishedApi internal fun JavaVirtualMachine.requireEnv(): JniEnvironment = env!! + +// Just for compiler plugin, too lazy to use the property +// @PublishedApi internal fun JniEnvironment.requireJavaVM(): JavaVirtualMachine = javaVM + diff --git a/knee-runtime/src/backendMain/kotlin/buffer/ByteBuffer.kt b/knee-runtime/src/backendMain/kotlin/buffer/ByteBuffer.kt new file mode 100644 index 0000000..7cbf122 --- /dev/null +++ b/knee-runtime/src/backendMain/kotlin/buffer/ByteBuffer.kt @@ -0,0 +1,96 @@ +package io.deepmedia.tools.knee.runtime.buffer + +import io.deepmedia.tools.knee.runtime.* +import io.deepmedia.tools.knee.runtime.compiler.setDirectBufferNativeOrder +import kotlinx.cinterop.* +import platform.android.jobject +import kotlin.native.ref.createCleaner + +class ByteBuffer internal constructor( + environment: JniEnvironment, + jobject: jobject, + storage: CArrayPointer?, + private val freeStorage: (CArrayPointer) -> Unit, + size: Int? +) { + + /** + * Called by the compiler when a buffer comes from JVM world. + * The [ByteBuffer] class here will create a strong reference out of the object + * and automatically delete it when this buffer is garbage collected. + */ + @Suppress("unused") + @PublishedApi + internal constructor(environment: JniEnvironment, jobject: jobject) : this( + environment = environment, + jobject = jobject, + storage = null, + freeStorage = { }, + size = null + ) + + /** + * Can be called by users to allocate a new [ByteBuffer] natively. + * It can be later be passed to JVM, and it will remain valid until [ByteBuffer.free] is called. + * After free, the buffer should not be accessed from JVM - data will be undefined, may crash. + * See `NewDirectByteBuffer` docs. + */ + constructor(environment: JniEnvironment, size: Int) : this( + environment = environment, + storage = nativeHeap.allocArray(size), + freeStorage = { nativeHeap.free(it) }, + size = size + ) + + /** + * Can be called by users to allocate a new [ByteBuffer] natively. + * It can be later be passed to JVM, and it will remain valid until the storage is valid. + * [freeStorage] should free the storage and make it invalid. + */ + constructor( + environment: JniEnvironment, + size: Int, + storage: CArrayPointer, + freeStorage: (CArrayPointer) -> Unit, + ) : this( + environment = environment, + jobject = environment.newDirectByteBuffer(storage, size.toLong()).also { + //https://bugs.openjdk.org/browse/JDK-5043362 + // need to call order(nativeOrder()) here, otherwise ByteBuffer is always BIG_ENDIAN + // when seen from the JVM side. Without it JVM users are required to do order() before reads/writes + environment.setDirectBufferNativeOrder(it) + }, + storage = storage, + freeStorage = freeStorage, + size = size + ) + + private val jvm = environment.javaVM + + @PublishedApi + internal val obj = environment.newGlobalRef(jobject).also { + // TODO: review this usage of deleteLocalRef, not clear it's needed. See other usages + environment.deleteLocalRef(jobject) + } + + // number of elements, not necessarily bytes + val size: Int = size ?: environment.getDirectBufferCapacity(obj).toInt() + + @Suppress("UNCHECKED_CAST") + val ptr: CArrayPointer = storage + ?: environment.getDirectBufferAddress(obj) as CArrayPointer + + @Suppress("unused") + private val objCleaner = createCleaner(this.jvm to this.obj) { (jvm, it) -> + val env = jvm.env ?: jvm.attachCurrentThread() + env.deleteGlobalRef(it) + } + + // don't want to add all Buffer (questionable) functions, let's just expose pointer instead. + // operator fun set(index: Int = 0, value: Byte) { ptr[index] = value } + // operator fun get(index: Int = 0): Byte { return ptr[index] } + + fun free() { + freeStorage(ptr) + } +} \ No newline at end of file diff --git a/knee-runtime/src/backendMain/kotlin/buffer/DoubleBuffer.kt b/knee-runtime/src/backendMain/kotlin/buffer/DoubleBuffer.kt new file mode 100644 index 0000000..4eca3e7 --- /dev/null +++ b/knee-runtime/src/backendMain/kotlin/buffer/DoubleBuffer.kt @@ -0,0 +1,26 @@ +package io.deepmedia.tools.knee.runtime.buffer + +import io.deepmedia.tools.knee.runtime.* +import kotlinx.cinterop.* +import platform.android.jobject + +class DoubleBuffer private constructor(private val bytes: ByteBuffer) { + + @PublishedApi internal constructor(environment: JniEnvironment, jobject: jobject) : this(ByteBuffer( + environment = environment, + jobject = jobject, + storage = null, + freeStorage = { }, + size = environment.getDirectBufferCapacity(jobject).toInt() * 8 + )) + + constructor(environment: JniEnvironment, size: Int) : this(ByteBuffer( + environment = environment, + size = size * 8 + )) + + @PublishedApi internal val obj get() = bytes.obj + val size: Int get() = bytes.size / 8 + val ptr: CArrayPointer = bytes.ptr.reinterpret() + fun free() = bytes.free() +} \ No newline at end of file diff --git a/knee-runtime/src/backendMain/kotlin/buffer/FloatBuffer.kt b/knee-runtime/src/backendMain/kotlin/buffer/FloatBuffer.kt new file mode 100644 index 0000000..0018efc --- /dev/null +++ b/knee-runtime/src/backendMain/kotlin/buffer/FloatBuffer.kt @@ -0,0 +1,26 @@ +package io.deepmedia.tools.knee.runtime.buffer + +import io.deepmedia.tools.knee.runtime.* +import kotlinx.cinterop.* +import platform.android.jobject + +class FloatBuffer private constructor(private val bytes: ByteBuffer) { + + @PublishedApi internal constructor(environment: JniEnvironment, jobject: jobject) : this(ByteBuffer( + environment = environment, + jobject = jobject, + storage = null, + freeStorage = { }, + size = environment.getDirectBufferCapacity(jobject).toInt() * 4 + )) + + constructor(environment: JniEnvironment, size: Int) : this(ByteBuffer( + environment = environment, + size = size * 4 + )) + + @PublishedApi internal val obj get() = bytes.obj + val size: Int get() = bytes.size / 4 + val ptr: CArrayPointer = bytes.ptr.reinterpret() + fun free() = bytes.free() +} \ No newline at end of file diff --git a/knee-runtime/src/backendMain/kotlin/buffer/IntBuffer.kt b/knee-runtime/src/backendMain/kotlin/buffer/IntBuffer.kt new file mode 100644 index 0000000..1c8cfa6 --- /dev/null +++ b/knee-runtime/src/backendMain/kotlin/buffer/IntBuffer.kt @@ -0,0 +1,26 @@ +package io.deepmedia.tools.knee.runtime.buffer + +import io.deepmedia.tools.knee.runtime.* +import kotlinx.cinterop.* +import platform.android.jobject + +class IntBuffer private constructor(private val bytes: ByteBuffer) { + + @PublishedApi internal constructor(environment: JniEnvironment, jobject: jobject) : this(ByteBuffer( + environment = environment, + jobject = jobject, + storage = null, + freeStorage = { }, + size = environment.getDirectBufferCapacity(jobject).toInt() * 4 + )) + + constructor(environment: JniEnvironment, size: Int) : this(ByteBuffer( + environment = environment, + size = size * 4 + )) + + @PublishedApi internal val obj get() = bytes.obj + val size: Int get() = bytes.size / 4 + val ptr: CArrayPointer = bytes.ptr.reinterpret() + fun free() = bytes.free() +} \ No newline at end of file diff --git a/knee-runtime/src/backendMain/kotlin/buffer/LongBuffer.kt b/knee-runtime/src/backendMain/kotlin/buffer/LongBuffer.kt new file mode 100644 index 0000000..5e6d918 --- /dev/null +++ b/knee-runtime/src/backendMain/kotlin/buffer/LongBuffer.kt @@ -0,0 +1,26 @@ +package io.deepmedia.tools.knee.runtime.buffer + +import io.deepmedia.tools.knee.runtime.* +import kotlinx.cinterop.* +import platform.android.jobject + +class LongBuffer private constructor(private val bytes: ByteBuffer) { + + @PublishedApi internal constructor(environment: JniEnvironment, jobject: jobject) : this(ByteBuffer( + environment = environment, + jobject = jobject, + storage = null, + freeStorage = { }, + size = environment.getDirectBufferCapacity(jobject).toInt() * 8 + )) + + constructor(environment: JniEnvironment, size: Int) : this(ByteBuffer( + environment = environment, + size = size * 8 + )) + + @PublishedApi internal val obj get() = bytes.obj + val size: Int get() = bytes.size / 8 + val ptr: CArrayPointer = bytes.ptr.reinterpret() + fun free() = bytes.free() +} \ No newline at end of file diff --git a/knee-runtime/src/backendMain/kotlin/collections/ArraySpec.kt b/knee-runtime/src/backendMain/kotlin/collections/ArraySpec.kt new file mode 100644 index 0000000..a6f7729 --- /dev/null +++ b/knee-runtime/src/backendMain/kotlin/collections/ArraySpec.kt @@ -0,0 +1,79 @@ +@file:Suppress("unused") + +package io.deepmedia.tools.knee.runtime.collections + +/** + * Sealed class for all of the existing array types e.g. [Array], [IntArray], [ByteArray], [LongArray], + * which unfortunately do not have a common super class. + */ +@PublishedApi +internal sealed class ArraySpec( + val empty: Type, + val builder: CollectionBuilder // a builder that can create 'Type' +) { + abstract fun sizeOf(array: Type): Int + abstract fun elementOf(array: Type, index: Int): Element + + fun build(from: Collection): Type { + val iterator = from.iterator() + return builder(from.size) { iterator.next() } + } + + object Bytes : ArraySpec(ByteArray(0), ::ByteArray) { + override fun sizeOf(array: ByteArray) = array.size + override fun elementOf(array: ByteArray, index: Int) = array[index] + // override fun build(from: Collection) = from.toByteArray() + } + + object Booleans : ArraySpec(BooleanArray(0), ::BooleanArray) { + override fun sizeOf(array: BooleanArray) = array.size + override fun elementOf(array: BooleanArray, index: Int) = array[index] + // override fun build(from: Collection) = from.toBooleanArray() + } + + @OptIn(ExperimentalUnsignedTypes::class) + object UBytes : ArraySpec(UByteArray(0), ::UByteArray) { + override fun sizeOf(array: UByteArray) = array.size + override fun elementOf(array: UByteArray, index: Int) = array[index] + // override fun build(from: Collection) = from.toUByteArray() + } + + object Ints : ArraySpec(IntArray(0), ::IntArray) { + override fun sizeOf(array: IntArray) = array.size + override fun elementOf(array: IntArray, index: Int) = array[index] + // override fun build(from: Collection) = from.toIntArray() + } + + object Longs : ArraySpec(LongArray(0), ::LongArray) { + override fun sizeOf(array: LongArray) = array.size + override fun elementOf(array: LongArray, index: Int) = array[index] + // override fun build(from: Collection) = from.toLongArray() + } + + object Floats : ArraySpec(FloatArray(0), ::FloatArray) { + override fun sizeOf(array: FloatArray) = array.size + override fun elementOf(array: FloatArray, index: Int) = array[index] + // override fun build(from: Collection) = from.toFloatArray() + } + + object Doubles : ArraySpec(DoubleArray(0), ::DoubleArray) { + override fun sizeOf(array: DoubleArray) = array.size + override fun elementOf(array: DoubleArray, index: Int) = array[index] + // override fun build(from: Collection) = from.toDoubleArray() + } + + class Typed( + empty: Array, + builder: CollectionBuilder>, + // private val maker: Collection.() -> Array + ) : ArraySpec, Element>(empty, builder) { + override fun sizeOf(array: Array) = array.size + override fun elementOf(array: Array, index: Int) = array[index] + // override fun build(from: Collection) = from.maker() + } +} + +@PublishedApi +internal inline fun typedArraySpec(): ArraySpec.Typed { + return ArraySpec.Typed(emptyArray(), ::Array) // { toTypedArray() } +} diff --git a/knee-runtime/src/backendMain/kotlin/collections/CollectionCodec.kt b/knee-runtime/src/backendMain/kotlin/collections/CollectionCodec.kt new file mode 100644 index 0000000..2dd120e --- /dev/null +++ b/knee-runtime/src/backendMain/kotlin/collections/CollectionCodec.kt @@ -0,0 +1,185 @@ +package io.deepmedia.tools.knee.runtime.collections + +import io.deepmedia.tools.knee.runtime.* +import io.deepmedia.tools.knee.runtime.compiler.ClassIds +import kotlinx.cinterop.* +import platform.android.JNI_ABORT +import platform.android.jarray +import platform.android.jobject + +internal typealias CollectionBuilder = (size: Int, elementAt: (Int) -> Element) -> Collection + +/** + * Maps a jarray from/to an external element type, and from/into different kinds of collection types. + * Use concrete subclasses: + * - [JObjectCollectionCodec] when the element is a jobject + * - one of the [SimpleCollectionCodec]s for primitive types + * - a [TransformingCollectionCodec] if elements must be transformed on the fly + */ +internal interface CollectionCodec { + + fun JniEnvironment.decodeIntoArray(array: jarray): Array + fun JniEnvironment.decodeIntoList(array: jarray): List + fun JniEnvironment.decodeIntoSet(array: jarray): Set + + fun JniEnvironment.encodeArray(array: Array): jarray + fun JniEnvironment.encodeList(list: List): jarray + fun JniEnvironment.encodeSet(set: Set): jarray +} + +internal abstract class BaseCollectionCodec( + protected val arraySpec: ArraySpec +) : CollectionCodec { + + final override fun JniEnvironment.decodeIntoArray(array: jarray): ArrayType { + val length = getArrayLength(array).takeIf { it > 0 } ?: return arraySpec.empty + return decodeIntoBuilder(array, length, arraySpec.builder) + } + + final override fun JniEnvironment.decodeIntoList(array: jarray): List { + val length = getArrayLength(array).takeIf { it > 0 } ?: return emptyList() + return decodeIntoBuilder(array, length, ::List) + } + + final override fun JniEnvironment.decodeIntoSet(array: jarray): Set { + val length = getArrayLength(array).takeIf { it > 0 } ?: return emptySet() + return decodeIntoBuilder(array, length) { size, itemAt -> + buildSet { for (i in 0 until size) add(itemAt(i)) } + } + } + + // We could use a builder-like solution for encoding too - a jarray builder. + // But primitive arrays have more efficient ways of filling data (setArrayRegion) + // then iterating over the elements one by one, so we use instantiate + fill approach. + + final override fun JniEnvironment.encodeArray(array: ArrayType): jarray { + val size = arraySpec.sizeOf(array) + return instantiate(size).also { if (size > 0) fillFromArray(it, array) } + } + + final override fun JniEnvironment.encodeList(list: List): jarray { + return instantiate(list.size).also { if (list.isNotEmpty()) fillFromList(it, list) } + } + + final override fun JniEnvironment.encodeSet(set: Set): jarray { + return instantiate(set.size).also { if (set.isNotEmpty()) fillFromSet(it, set) } + } + + protected abstract fun JniEnvironment.decodeIntoBuilder(array: jarray, length: Int, builder: CollectionBuilder): Result + protected abstract fun JniEnvironment.instantiate(size: Int): jarray + protected abstract fun JniEnvironment.fillFromArray(jarray: jarray, array: ArrayType) + protected abstract fun JniEnvironment.fillFromList(jarray: jarray, list: List) + protected abstract fun JniEnvironment.fillFromSet(jarray: jarray, set: Set) +} + +@PublishedApi +internal class JObjectCollectionCodec(private val jvmClassName: String): BaseCollectionCodec>( + typedArraySpec() +) { + + override fun JniEnvironment.decodeIntoBuilder(array: jarray, length: Int, builder: CollectionBuilder): Result { + return builder(length) { getObjectArrayElement(array, it) } + } + + override fun JniEnvironment.instantiate(size: Int): jarray = newObjectArray(size, + ClassIds.get(this, jvmClassName) + ) + + override fun JniEnvironment.fillFromArray(jarray: jarray, array: Array) { + array.forEachIndexed { index, obj -> setObjectArrayElement(jarray, index, obj) } + } + + override fun JniEnvironment.fillFromList(jarray: jarray, list: List) { + list.forEachIndexed { index, obj -> setObjectArrayElement(jarray, index, obj) } + } + + override fun JniEnvironment.fillFromSet(jarray: jarray, set: Set) { + set.forEachIndexed { index, obj -> setObjectArrayElement(jarray, index, obj) } + } +} + +@Suppress("unused") +@PublishedApi +internal sealed class SimpleCollectionCodec( + arraySpec: ArraySpec, + private val arrayToRef: ArrayType.(Int) -> CValuesRef, + private val read: CPointer.(Int) -> Element, + private val newInstance: JniEnvironment.(Int) -> jarray, + private val setArrayRegion: JniEnvironment.(jarray, Int, Int, CPointer) -> Unit +) : BaseCollectionCodec(arraySpec) { + + override fun JniEnvironment.decodeIntoBuilder(array: jarray, length: Int, builder: CollectionBuilder): Result = usePrimitiveArrayCritical(array, JNI_ABORT) { handle -> + val cast = handle.reinterpret() + builder(length) { cast.read(it) } + } + + override fun JniEnvironment.instantiate(size: Int): jarray = newInstance(size) + override fun JniEnvironment.fillFromList(jarray: jarray, list: List) = fillFromArray(jarray, arraySpec.build(list)) + override fun JniEnvironment.fillFromSet(jarray: jarray, set: Set) = fillFromArray(jarray, arraySpec.build(set)) + override fun JniEnvironment.fillFromArray(jarray: jarray, array: ArrayType) = memScoped { + val pointer = array.arrayToRef(0).getPointer(this) + setArrayRegion(jarray, 0, arraySpec.sizeOf(array), pointer) + } +} + +/** Converts [ByteArray] <-> [jarray] */ +@PublishedApi +internal object ByteCollectionCodec : SimpleCollectionCodec(ArraySpec.Bytes, ByteArray::refTo, { this[it] }, JniEnvironment::newByteArray, JniEnvironment::setByteArrayRegion) + +/** Converts [IntArray] <-> [jarray] */ +@PublishedApi +internal object IntCollectionCodec : SimpleCollectionCodec(ArraySpec.Ints, IntArray::refTo, { this[it] }, JniEnvironment::newIntArray, JniEnvironment::setIntArrayRegion) + +/** Converts [LongArray] <-> [jarray] */ +@PublishedApi +internal object LongCollectionCodec : SimpleCollectionCodec(ArraySpec.Longs, LongArray::refTo, { this[it] }, JniEnvironment::newLongArray, JniEnvironment::setLongArrayRegion) + +/** Converts [FloatArray] <-> [jarray] */ +@PublishedApi +internal object FloatCollectionCodec : SimpleCollectionCodec(ArraySpec.Floats, FloatArray::refTo, { this[it] }, JniEnvironment::newFloatArray, JniEnvironment::setFloatArrayRegion) + +/** Converts [DoubleArray] <-> [jarray] */ +@PublishedApi +internal object DoubleCollectionCodec : SimpleCollectionCodec(ArraySpec.Doubles, DoubleArray::refTo, { this[it] }, JniEnvironment::newDoubleArray, JniEnvironment::setDoubleArrayRegion) + +/** Converts [UByteArray] <-> [jarray]. Used for booleans */ +@PublishedApi +internal object UByteCollectionCodec : SimpleCollectionCodec(ArraySpec.UBytes, UByteArray::refTo, { this[it] }, JniEnvironment::newBooleanArray, JniEnvironment::setBooleanArrayRegion) + +@PublishedApi +internal class TransformingCollectionCodec( + private val source: CollectionCodec, + private val decodedArraySpec: ArraySpec, + private val decodeElement: (EncodedElement) -> DecodedElement, + private val encodeElement: (DecodedElement) -> EncodedElement +) : CollectionCodec { + + // TODO: encoded element might be a jobject. We need to make room for the references + // both when encoding and when decoding, or use a Sequence-like object and call deleteLocalRef after mapping + + override fun JniEnvironment.decodeIntoList(array: jarray): List { + val encodedList = with(source) { decodeIntoList(array) } + return encodedList.map(decodeElement) + } + + override fun JniEnvironment.decodeIntoSet(array: jarray): Set { + val encodedSet = with(source) { decodeIntoSet(array) } + return encodedSet.mapTo(mutableSetOf(), decodeElement) + } + + override fun JniEnvironment.decodeIntoArray(array: jarray): DecodedArray = + decodedArraySpec.build(decodeIntoList(array)) + + override fun JniEnvironment.encodeList(list: List): jarray { + return with(source) { encodeList(list.map(encodeElement)) } + } + + override fun JniEnvironment.encodeSet(set: Set): jarray { + return with(source) { encodeList(set.map(encodeElement)) } + } + + override fun JniEnvironment.encodeArray(array: DecodedArray): jarray { + val list = List(decodedArraySpec.sizeOf(array)) { encodeElement(decodedArraySpec.elementOf(array, it)) } + return with(source) { encodeList(list) } + } +} diff --git a/knee-runtime/src/backendMain/kotlin/compiler/BoxMethods.kt b/knee-runtime/src/backendMain/kotlin/compiler/BoxMethods.kt new file mode 100644 index 0000000..e0ded0e --- /dev/null +++ b/knee-runtime/src/backendMain/kotlin/compiler/BoxMethods.kt @@ -0,0 +1,38 @@ +@file:Suppress("unused") + +package io.deepmedia.tools.knee.runtime.compiler + +import io.deepmedia.tools.knee.runtime.* +import platform.android.* + + +internal fun initBoxMethods(environment: JniEnvironment) = with(BoxMethods) { + longConstructor = MethodIds.get(environment, "java.lang.Long", "", "(J)V", false) + longValue = MethodIds.get(environment, "java.lang.Long", "longValue", "()J", false) + intConstructor = MethodIds.get(environment, "java.lang.Integer", "", "(I)V", false) + intValue = MethodIds.get(environment, "java.lang.Integer", "intValue", "()I", false) + byteConstructor = MethodIds.get(environment, "java.lang.Byte", "", "(B)V", false) + byteValue = MethodIds.get(environment, "java.lang.Byte", "byteValue", "()B", false) + boolConstructor = MethodIds.get(environment, "java.lang.Boolean", "", "(Z)V", false) + boolValue = MethodIds.get(environment, "java.lang.Boolean", "booleanValue", "()Z", false) + doubleConstructor = MethodIds.get(environment, "java.lang.Double", "", "(D)V", false) + doubleValue = MethodIds.get(environment, "java.lang.Double", "doubleValue", "()D", false) + floatConstructor = MethodIds.get(environment, "java.lang.Float", "", "(F)V", false) + floatValue = MethodIds.get(environment, "java.lang.Float", "floatValue", "()F", false) +} + +@PublishedApi +internal object BoxMethods { + lateinit var longConstructor: jmethodID + lateinit var longValue: jmethodID + lateinit var intConstructor: jmethodID + lateinit var intValue: jmethodID + lateinit var byteConstructor: jmethodID + lateinit var byteValue: jmethodID + lateinit var boolConstructor: jmethodID + lateinit var boolValue: jmethodID + lateinit var doubleConstructor: jmethodID + lateinit var doubleValue: jmethodID + lateinit var floatConstructor: jmethodID + lateinit var floatValue: jmethodID +} diff --git a/knee-runtime/src/backendMain/kotlin/compiler/Buffers.kn.kt b/knee-runtime/src/backendMain/kotlin/compiler/Buffers.kn.kt new file mode 100644 index 0000000..07a74a7 --- /dev/null +++ b/knee-runtime/src/backendMain/kotlin/compiler/Buffers.kn.kt @@ -0,0 +1,24 @@ +package io.deepmedia.tools.knee.runtime.compiler + +import io.deepmedia.tools.knee.runtime.* +import platform.android.* + + +private lateinit var bufferUtils: jclass +private lateinit var setByteBufferOrderMethod: jmethodID + +internal fun initBuffers(environment: JniEnvironment) { + bufferUtils = ClassIds.get(environment, "io.deepmedia.tools.knee.runtime.compiler.KneeBuffers") + setByteBufferOrderMethod = MethodIds.get( + env = environment, + classFqn = "io.deepmedia.tools.knee.runtime.compiler.KneeBuffers", + methodName = "setNativeOrder", + methodSignature = "(Ljava/nio/ByteBuffer;)V", + static = true, + classObject = bufferUtils + ) +} + +internal fun JniEnvironment.setDirectBufferNativeOrder(buffer: jobject) { + callStaticVoidMethod(bufferUtils, setByteBufferOrderMethod, buffer) +} diff --git a/knee-runtime/src/backendMain/kotlin/compiler/Cache.kt b/knee-runtime/src/backendMain/kotlin/compiler/Cache.kt new file mode 100644 index 0000000..44c9f16 --- /dev/null +++ b/knee-runtime/src/backendMain/kotlin/compiler/Cache.kt @@ -0,0 +1,92 @@ +package io.deepmedia.tools.knee.runtime.compiler + +import io.deepmedia.tools.knee.runtime.* +import kotlinx.atomicfu.locks.ReentrantLock +import kotlinx.atomicfu.locks.reentrantLock +import kotlinx.atomicfu.locks.withLock +import platform.android.jclass +import platform.android.jfieldID +import platform.android.jmethodID + +private class Cache { + private val map = mutableMapOf() + private val lock = reentrantLock() + inline fun get(key: String, defaultValue: () -> Data): Data { + map[key]?.let { return it } + return lock.withLock { map.getOrPut(key, defaultValue) } + } +} + +@PublishedApi +internal object MethodIds { + private val cache = Cache() + + // classFqn: java.lang.String + fun get( + env: JniEnvironment, + classFqn: String, + methodName: String, + methodSignature: String, + static: Boolean, + classObject: jclass? = null, + ): jmethodID { + val key = "$classFqn::$methodName::$methodSignature::$static" + return cache.get(key) { + val jclass = classObject ?: ClassIds.get(env, classFqn) + if (static) env.getStaticMethodId(jclass, methodName, methodSignature) + else env.getMethodId(jclass, methodName, methodSignature) + } + } + + // NOTE: doesn't work for constructor of `inner` class + // https://stackoverflow.com/a/25363953 + fun getConstructor( + env: JniEnvironment, + argsSignature: String, + classFqn: String, + classObject: jclass? = null, + ): jmethodID = get( + env = env, + classFqn = classFqn, + methodName = "", + methodSignature = "($argsSignature)V", + static = false, + classObject = classObject + ) +} + +@PublishedApi +internal object ClassIds { + private val cache = Cache() + + /** for example. 'java.lang.String' */ + fun get(env: JniEnvironment, className: String): jclass { + return cache.get(className) { + // Safe deleteLocalRef: after find class it's needed to dispose the resource + val klass = env.findClass(className) + env.newGlobalRef(klass).also { env.deleteLocalRef(klass) } + } + } +} + +@PublishedApi +internal object FieldIds { + private val cache = Cache() + + // classFqn: java.lang.String + fun get( + env: JniEnvironment, + classFqn: String, + fieldName: String, + fieldSignature: String, + static: Boolean, + classObject: jclass? = null, + ): jfieldID { + val key = "$classFqn::$fieldName::$fieldSignature::$static" + return cache.get(key) { + val jclass = classObject ?: ClassIds.get(env, classFqn) + if (static) env.getStaticFieldId(jclass, fieldName, fieldSignature) + else env.getFieldId(jclass, fieldName, fieldSignature) + } + } +} \ No newline at end of file diff --git a/knee-runtime/src/backendMain/kotlin/compiler/Exceptions.kn.kt b/knee-runtime/src/backendMain/kotlin/compiler/Exceptions.kn.kt new file mode 100644 index 0000000..7b8a483 --- /dev/null +++ b/knee-runtime/src/backendMain/kotlin/compiler/Exceptions.kn.kt @@ -0,0 +1,201 @@ +package io.deepmedia.tools.knee.runtime.compiler + +import io.deepmedia.tools.knee.runtime.* +import io.deepmedia.tools.knee.runtime.types.decodeClass +import io.deepmedia.tools.knee.runtime.types.decodeString +import io.deepmedia.tools.knee.runtime.types.encodeClass +import io.deepmedia.tools.knee.runtime.types.encodeString +import kotlinx.cinterop.* +import platform.android.jclass +import platform.android.jmethodID +import platform.android.jthrowable +import kotlin.coroutines.cancellation.CancellationException +import kotlin.native.ref.createCleaner + +private lateinit var exceptionToken: jclass +private lateinit var throwableGetMessage: jmethodID +private lateinit var classGetName: jmethodID +private lateinit var exceptionTokenCreate: jmethodID +private lateinit var exceptionTokenGet: jmethodID + +internal fun initExceptions(environment: JniEnvironment) { + environment.registerNatives( + classFqn = "io.deepmedia.tools.knee.runtime.compiler.KneeKnExceptionToken", + bindings = arrayOf(JniNativeMethod( + name = "clear", + signature = "(J)V", + pointer = staticCFunction { _, _, addr -> + val ref = addr.toCPointer()?.asStableRef() ?: return@staticCFunction + runCatching { ref.dispose() } + } + )) + ) + throwableGetMessage = MethodIds.get(environment, "java.lang.Throwable", "getMessage", "()Ljava/lang/String;", false) + classGetName = MethodIds.get(environment, "java.lang.Class", "getName", "()Ljava/lang/String;", false) + exceptionToken = ClassIds.get(environment, "io.deepmedia.tools.knee.runtime.compiler.KneeKnExceptionToken") + exceptionTokenCreate = MethodIds.get(environment, "io.deepmedia.tools.knee.runtime.compiler.KneeKnExceptionToken", "", "(J)V", false, exceptionToken) + exceptionTokenGet = MethodIds.get(environment, "io.deepmedia.tools.knee.runtime.compiler.KneeKnExceptionToken", "get", "(Ljava/lang/Throwable;)J", static = true, exceptionToken) +} + +/** + * Something failed in native code. We proceed as follows: + * + * - N1. If this was originally a JVM failure (see JVM2. in [processJvmException]), just rethrow + * the JVM exception as is so we are fully transparent, and code that relies on exception + * checks (e.g. coroutines collectWhile) will still work. + * + * - N2. Otherwise, this native failure must be transformed into a JVM exception. + * We want to inject the native failure into it as its [Throwable.cause], thus overriding + * the current cause. This is needed to implement the 2-way transparency behavior described above. + * + * When transforming into a JVM exception, we should do either of the following: + * + * - N2.1. Check if this throwable represents a @KneeClass type! In that case we can simply create + * an instance of the cloned type, after encoding this throwable as a long with encodeClass. + * + * - N2.2 Try to respect the exception type for common stdlib errors. + * We currently identify [CancellationException]s this way, and reuse the same [Throwable.message]. + * + * - N2.3 Fallback to plain [RuntimeException], reusing at least the [Throwable.message]. + */ +internal fun JniEnvironment.processNativeException(throwable: Throwable): jthrowable { + run { + val original = throwable.cause as? KneeJvmExceptionToken + if (original != null) { + // This K/N Throwable actually came from JVM. Since we stored the original + // JVM exception, just use that instead of creating a new one. This makes exception + // totally transparent which can be needed (example: flow.collectWhile). + return original.reference + } + } + + run { + val qualifiedName = throwable::class.qualifiedName + val serializableExceptions = initializationData?.exceptions + if (qualifiedName != null && serializableExceptions != null) { + val match = serializableExceptions.firstOrNull { it.nativeFqn == qualifiedName } + if (match != null) { + // NOTE: constructor will be `internal` and part of another module, but we use `PublishedApi` + // on the declaration and so it should be available here. About access, JNI doesn't check that. + val jvmClass = ClassIds.get(this, match.jvmFqn) + val jvmConstructor = MethodIds.getConstructor(this, "J", match.jvmFqn, jvmClass) + val jvmInstance = newObject(jvmClass, jvmConstructor, encodeClass(throwable)) + return jvmInstance + } + } + } + + // Fallback: create a new exception. Try respecting the class type and the message. + // TODO: support more types + val className = when (throwable) { + is CancellationException -> "java.util.concurrent.CancellationException" + else -> "java.lang.RuntimeException" + } + val message = throwable.message ?: "Unexpected native exception." + val klass = ClassIds.get(this, className) + val constructor = MethodIds.getConstructor( + this, + classFqn = className, + argsSignature = "Ljava/lang/String;Ljava/lang/Throwable;", + classObject = klass + ) + val token = StableRef.create(throwable).asCPointer().toLong() + val tokenHolder = newObject(exceptionToken, exceptionTokenCreate, token) + return newObject(klass, constructor, encodeString(this, message), tokenHolder) +} + + +/** + * Something failed in JVM code. We proceed as follows: + * + * - JVM1. If this was originally a native failure (see N2. in [processNativeException]), just rethrow + * the native exception as is so we are fully transparent, and code that relies on exception + * checks (e.g. coroutines collectWhile) will still work. + * + * - JVM2. Otherwise, this JVM failure must be transformed into a native exception. + * We want to inject the JVM failure into it as its [Throwable.cause], thus overriding + * the current cause. This is needed to implement the 2-way transparency behavior described above. + * + * When transforming into a native exception, we should do either of the following: + * + * - JVM2.1. Check if this throwable represents a @KneeClass type! In that case we can simply retrieve + * an instance of the original native type, after fetching the jthrowable's long and decoding with decodeClass. + * + * - JVM2.2. Try to respect the exception type for common stdlib errors. + * We currently identify [CancellationException]s this way, and reuse the same [Throwable.message]. + * + * - jVM2.3. Fallback to plain [RuntimeException], reusing at least the [Throwable.message]. + */ +internal fun JniEnvironment.processJvmException(throwable: jthrowable?): Throwable { + if (throwable == null) return RuntimeException("Unexpected JVM exception.") + + // JVM1 + run { + val original = callStaticLongMethod(exceptionToken, exceptionTokenGet, throwable) + if (original != 0L) { + // This JVM Throwable actually came from K/N. Since we stored the original + // K/N exception, just use that instead of creating a new one. This makes exception + // totally transparent which can be needed (example: flow.collectWhile). + val res = original.toCPointer()?.asStableRef()?.get() + if (res != null) return res + } + } + + val qualifiedName = decodeString(this, callObjectMethod( + jobject = getObjectClass(throwable), + method = classGetName + )!!) + + // JVM2.1 + run { + val serializableExceptions = initializationData?.exceptions + if (serializableExceptions != null) { + val match = serializableExceptions.firstOrNull { it.jvmFqnWithDots == qualifiedName } + if (match != null) { + val jvmHandleField = FieldIds.get(this, match.jvmFqn, "\$knee","J", false) + val handle = getLongField(throwable, jvmHandleField) + return decodeClass(handle) + } + } + } + + // JVM2.2, JVM2.3 + val cause = KneeJvmExceptionToken(this, throwable) + val message: String = run { + val obj = callObjectMethod(throwable, throwableGetMessage) ?: return@run "Unexpected JVM exception." + val chars = getStringUTFChars(obj) + chars.toKStringFromUtf8().also { releaseStringUTFChars(obj, chars) } + } + // TODO: support more types + return when (qualifiedName) { + "java.util.concurrent.CancellationException" -> CancellationException(message, cause) + else -> RuntimeException(message, cause) + } +} + +// Holds an exception that happened on the JVM side +private class KneeJvmExceptionToken(environment: JniEnvironment, throwable: jthrowable) : RuntimeException() { + val reference = environment.newGlobalRef(throwable) + private val jvm = environment.javaVM + @Suppress("unused") + private val referenceCleaner = createCleaner(this.jvm to this.reference) { (jvm, it) -> + val env = jvm.env ?: jvm.attachCurrentThread() + env.deleteGlobalRef(it) + } +} + +// Utilities used by the compiler + +@PublishedApi +internal data class SerializableException( + val nativeFqn: String, // com.package.MyClassName + val jvmFqn: String // com/package/MyClassCodegenName +) { + val jvmFqnWithDots by lazy { jvmFqn.replace("/", ".") } +} + +@Suppress("unused") +@PublishedApi +internal fun JniEnvironment.rethrowNativeException(throwable: Throwable) { + `throw`(processNativeException(throwable)) +} diff --git a/knee-runtime/src/backendMain/kotlin/compiler/Instances.kn.kt b/knee-runtime/src/backendMain/kotlin/compiler/Instances.kn.kt new file mode 100644 index 0000000..e5311ef --- /dev/null +++ b/knee-runtime/src/backendMain/kotlin/compiler/Instances.kn.kt @@ -0,0 +1,50 @@ +package io.deepmedia.tools.knee.runtime.compiler + +import io.deepmedia.tools.knee.runtime.* +import kotlinx.cinterop.* +import platform.android.* + + +// internal lateinit var kneeWrapInstance: jmethodID // public fun kneeWrapInstance(ref: Long, className: String): Any? +// internal lateinit var kneeUnwrapInstance: jmethodID // public fun kneeUnwrapInstance(instance: Any): Long + +private const val InstancesKtFqn = "io.deepmedia.tools.knee.runtime.compiler.InstancesKt" + +internal fun initInstances(environment: JniEnvironment) { + // kneeWrapInstance = MethodIds.get(environment, InstancesKtFqn, "kneeWrapInstance", "(JLjava/lang/String;)Ljava/lang/Object;", true) + // kneeUnwrapInstance = MethodIds.get(environment, InstancesKtFqn, "kneeUnwrapInstance", "(Ljava/lang/Object;)J", true) + environment.registerNatives( + classFqn = InstancesKtFqn, + JniNativeMethod( + name = "kneeDisposeInstance", + signature = "(J)V", + pointer = staticCFunction { _, _, ref -> + runCatching { ref.toCPointer()!!.asStableRef().dispose() } + } + ), + JniNativeMethod( + name = "kneeDescribeInstance", + signature = "(J)Ljava/lang/String;", + pointer = staticCFunction { env, _, ref -> + ref.toCPointer()!!.asStableRef().get().toString().let { env.newStringUTF(it) } + } + ), + JniNativeMethod( + name = "kneeHashInstance", + signature = "(J)I", + pointer = staticCFunction { env, _, ref -> + ref.toCPointer()!!.asStableRef().get().hashCode() + } + ), + JniNativeMethod( + name = "kneeCompareInstance", + signature = "(JJ)Z", + pointer = staticCFunction { env, _, ref0, ref1 -> + val i0 = ref0.toCPointer()?.asStableRef()?.get() + val i1 = ref1.toCPointer()?.asStableRef()?.get() + if (i0 != null && i0 == i1) 1u else 0u + } + ) + ) +} + diff --git a/knee-runtime/src/backendMain/kotlin/compiler/JvmInterfaceWrapper.kt b/knee-runtime/src/backendMain/kotlin/compiler/JvmInterfaceWrapper.kt new file mode 100644 index 0000000..7cdcb39 --- /dev/null +++ b/knee-runtime/src/backendMain/kotlin/compiler/JvmInterfaceWrapper.kt @@ -0,0 +1,84 @@ +package io.deepmedia.tools.knee.runtime.compiler + +import io.deepmedia.tools.knee.runtime.* +import io.deepmedia.tools.knee.runtime.types.decodeString +import platform.android.* +import kotlin.native.ref.createCleaner + +@Suppress("unused") +@PublishedApi +internal open class JvmInterfaceWrapper( + environment: JniEnvironment, + wrapped: jobject, + interfaceFqn: String, + private val methodOwnerFqn: String, // we write methods in the *Impl companion object + vararg methodsAndSignatures: String // alternate method name and signature. Just to be easier to pass from IR +) { + // accessed by IR + val virtualMachine = environment.javaVM + + // needed to return the wrapped object to JVM + // TODO: the deleteLocalRef below creates the warning: Attempt to remove non-JNI local reference + // When we, for example, pass a jobject to native and create an interface out of it. Concretely we are calling + // deleteLocalRef on the jobject from within the method, which is not needed. + // Same for almost all other usages of deleteLocalRef we do: it should be called by who creates + // We need to pass more information to codecs, to understand whether the resource should be released or not + // or maybe no resource needs to be released at all + val jvmInterfaceObject = environment.newGlobalRef(wrapped).also { + environment.deleteLocalRef(wrapped) + } + + private val jvmInterfaceCleaner = createCleaner(this.virtualMachine to this.jvmInterfaceObject) { (virtualMachine, it) -> + // looking at K/N code, looks like cleaner blocks are invoked on a special cleaner worker that should be long-lived, + // in which case there's no harm to call attachCurrentThread() without detach. And since many + // objects will be cleaned, calling attach+detach every time would easily hurt performance. + // TODO: this still true? ^ + val env = virtualMachine.env ?: virtualMachine.attachCurrentThread() + env.deleteGlobalRef(it) + } + + private val equals: jmethodID + private val hashCode: jmethodID + private val toString: jmethodID + init { + val jclass = ClassIds.get(environment, interfaceFqn) + equals = MethodIds.get(environment, interfaceFqn, "equals", "(Ljava/lang/Object;)Z", false, jclass) + hashCode = MethodIds.get(environment, interfaceFqn, "hashCode", "()I", false, jclass) + toString = MethodIds.get(environment, interfaceFqn, "toString", "()Ljava/lang/String;", false, jclass) + } + + private val methodIds: Map = run { + val count = methodsAndSignatures.size / 2 + (0 until count).associate { + val name = methodsAndSignatures[it * 2] + val signature = methodsAndSignatures[it * 2 + 1] + "$name::$signature" to MethodIds.get(environment, methodOwnerFqn, name, signature, static = true, methodOwnerClass) + } + } + + // needed to call methods using env.callStatic***Method() + // accessed from IR + val methodOwnerClass = ClassIds.get(environment, methodOwnerFqn) + + // key is name::signature + fun method(key: String): jmethodID { + return checkNotNull(methodIds[key]) { "Method $key not found. Available: ${methodIds.keys}" } + } + + override fun equals(other: Any?): Boolean { + if (other === this) return true + if (other !is JvmInterfaceWrapper<*>) return false + return virtualMachine.useEnv { it.callBooleanMethod(jvmInterfaceObject, equals, other.jvmInterfaceObject) } == JNI_TRUE.toUByte() + } + + override fun hashCode(): Int { + return virtualMachine.useEnv { it.callIntMethod(jvmInterfaceObject, hashCode) } + } + + override fun toString(): String { + return virtualMachine.useEnv { + val jstring = it.callObjectMethod(jvmInterfaceObject, toString) + decodeString(it, jstring!!) + } + } +} \ No newline at end of file diff --git a/knee-runtime/src/backendMain/kotlin/compiler/Suspend.kn.kt b/knee-runtime/src/backendMain/kotlin/compiler/Suspend.kn.kt new file mode 100644 index 0000000..922e060 --- /dev/null +++ b/knee-runtime/src/backendMain/kotlin/compiler/Suspend.kn.kt @@ -0,0 +1,233 @@ +package io.deepmedia.tools.knee.runtime.compiler + +import io.deepmedia.tools.knee.runtime.* +import kotlinx.cinterop.* +import kotlinx.coroutines.* +import platform.android.* +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +private lateinit var cancelInvocationMethod: jmethodID + +internal fun initSuspend(environment: JniEnvironment) { + environment.registerNatives( + classFqn = "io.deepmedia.tools.knee.runtime.compiler.KneeSuspendInvoker", + bindings = arrayOf(JniNativeMethod( + name = "sendCancellation", + signature = "(J)V", + pointer = staticCFunction { _, _, addr -> + KneeSuspendInvocation.cancel(addr) + } + )) + ) + cancelInvocationMethod = MethodIds.get( + env = environment, + classFqn = "io.deepmedia.tools.knee.runtime.compiler.KneeSuspendInvocation", + methodName = "receiveCancellation", + methodSignature = "()V", + static = false + ) + // private external fun sendFailure(invoker: Long, error: String, cancellation: Boolean) => (JLjava/lang/String;Z)V + // private external fun sendFailure(invoker: Long, error: Throwable) => (JLjava/lang/Throwable;)V + environment.registerNatives( + classFqn = "io.deepmedia.tools.knee.runtime.compiler.KneeSuspendInvocation", + bindings = arrayOf( + JniNativeMethod( + name = "sendFailure", + signature = "(JLjava/lang/Throwable;)V", + pointer = staticCFunction { env, _, invoker, error -> + val invokerObject = runCatching { + invoker.toCPointer()!!.asStableRef>().get() + }.getOrNull() ?: return@staticCFunction + invokerObject.receiveFailure(env, error) + } + ), + JniNativeMethod( + name = "sendSuccess", + signature = "(JLjava/lang/Object;)V", + pointer = staticCFunction { env, _, invoker, genericResult -> + val invokerObject = runCatching { + invoker.toCPointer()!!.asStableRef>().get() + }.getOrNull() ?: return@staticCFunction + invokerObject.receiveSuccess(env, genericResult) + } + ) + ) + ) +} + +// REGULAR SUSPEND SUPPORT (JvmSuspend) + +private val KneeScope = CoroutineScope(Dispatchers.Unconfined + CoroutineName("Knee")) + +@Suppress("unused") +@PublishedApi +internal fun kneeInvokeJvmSuspend( + environment: JniEnvironment, + invoker: jobject, + // jniReturnTypeSignature: String, // JNI signature of JniReturnType, e.g. Ljava/lang/Object; + block: suspend () -> LocalReturnType, + encoder: (JniEnvironment, LocalReturnType) -> JniReturnType, +): Long = KneeSuspendInvocation(environment, invoker, block, encoder).address + +private class KneeSuspendInvocation( + environment: JniEnvironment, + invoker: jobject, + block: suspend () -> Local, + encoder: (JniEnvironment, Local) -> Jni, +) { + private val jvm = environment.javaVM + private val selfRef = StableRef.create(this) + private val invokerRef = environment.newGlobalRef(invoker) + + val address = selfRef.asCPointer().toLong() + + // Early fetch the jmethods because it's not safe to do it in useEnv { } later, + // the JVM might use the wrong class loader: + // https://developer.android.com/training/articles/perf-jni#faq:-why-didnt-findclass-find-my-class + private val complete = getReceiveSuccessMethod(environment) // , jniTypeSignature) + private val fail = getReceiveFailureMethod(environment) + // private val returnsUnit = jniTypeSignature.isEmpty() + + // Note: undispatched is very important, block() needs the same thread so that JniEnvironment + // is still valid for example. + private val job = KneeScope.launch(start = CoroutineStart.UNDISPATCHED) { + try { + val result = block() + jvm.useEnv { env -> + val encoded = encoder(env, result) + env.callVoidMethod(invokerRef, complete, encoded) + } + } catch (e: Throwable) { + jvm.useEnv { env -> + val jthrowable = env.processNativeException(e) + env.callVoidMethod(invokerRef, fail, jthrowable) + // val message = env.newStringUTF("Native failure: ${e.message}") + // val cancellation: jboolean = if (e is CancellationException) 1u else 0u + // env.callVoidMethod(invokerRef, fail, message, cancellation) + } + } finally { + dispose() + } + } + + fun dispose() { + runCatching { selfRef.dispose() } + runCatching { jvm.useEnv { it.deleteGlobalRef(invokerRef) } } + } + + companion object { + /** + * TODO: this is not 100% safe, passing a disposed address here can cause segfault + * The biggest problem is that neither asStableRef nor StableRef.get() fail. Only when we dereference + * the invocation the system fails with a segmentation fault. + * + * Fixed the most frequent crash by checking for cancellation.isCompleted in JVM's KneeSuspendInvoker. + * So it won't even send the cancel call if it already received a result. + * + * There is still margin for improvement though because in a concurrent scenario, we might + * dispose the stable ref while this function is being executed (or slightly before). + * Check KneeSuspendInvoker.sendCancellationSafe(). + */ + fun cancel(address: Long) = runCatching { + val ref = address.toCPointer()!!.asStableRef>() + val inv: KneeSuspendInvocation<*,*> = ref.get() + inv.job.cancel() + } + + // This is an extra layer of cache, should be faster than creating MethodKeys for each call + // we have a very limited set of functions that we want to store. Does this make sense? IDK + private const val invokerFqn = "io.deepmedia.tools.knee.runtime.compiler.KneeSuspendInvoker" + // private val receiveSuccessMethods = mutableMapOf() + private var receiveSuccessMethod: jmethodID? = null + private var receiveFailureMethod: jmethodID? = null + + /* private fun getReceiveSuccessMethod(environment: JniEnvironment, rawTypeSignature: String): jmethodID { + return receiveSuccessMethods.getOrPut(rawTypeSignature) { + MethodIds.get(environment, invokerFqn, "receiveSuccess", "($rawTypeSignature)V", false) + } + } */ + + private fun getReceiveSuccessMethod(environment: JniEnvironment): jmethodID { + if (receiveSuccessMethod == null) { // String, Boolean + receiveSuccessMethod = MethodIds.get(environment, invokerFqn, "receiveSuccess", "(Ljava/lang/Object;)V", false) + } + return receiveSuccessMethod!! + } + + private fun getReceiveFailureMethod(environment: JniEnvironment): jmethodID { + if (receiveFailureMethod == null) { // String, Boolean + // receiveFailureMethod = MethodIds.get(environment, invokerFqn, "receiveFailure", "(Ljava/lang/String;Z)V", false) + receiveFailureMethod = MethodIds.get(environment, invokerFqn, "receiveFailure", "(Ljava/lang/Throwable;)V", false) + } + return receiveFailureMethod!! + } + } +} + +// REVERSE SUSPEND SUPPORT (KnSuspend) + +@PublishedApi +@Suppress("unused") +internal suspend fun kneeInvokeKnSuspend( + virtualMachine: JavaVirtualMachine, + block: (JniEnvironment, Long) -> jobject, + decoder: (JniEnvironment, Encoded) -> Decoded +): Decoded { + return suspendCancellableCoroutine { cont -> + // invoker must decode otherwise we might end up releasing the environment with useEnv before decode + val invoker = KneeSuspendInvoker(virtualMachine, cont, decoder) + val invocation: jobject? = virtualMachine.useEnv { env -> + val weak = block(env, invoker.address) + if (invoker.completed) null else { + // TODO: review this usage of deleteLocalRef, not clear it's needed. See other usages + env.newGlobalRef(weak).also { env.deleteLocalRef(weak) } + } + } + if (invocation != null) { + cont.invokeOnCancellation { + invoker.sendCancellation(invocation) + } + // Not clear when we should delete the global reference. Not sure if the job has the same lifecycle + // of this coroutine, might live longer. But not a big issue. On the other hand passing the invocation to invoker + // to be released during receiveSuccess() or receiveFailure() IS a problem, because we would need a mutex + // for a 100% safe implementation. + // Current solution is bad though in that it attaches/detaches the JNI environment an extra time. + cont.context.job.invokeOnCompletion { + virtualMachine.useEnv { it.deleteGlobalRef(invocation) } + } + } + } +} + +@PublishedApi +internal class KneeSuspendInvoker( + private val jvm: JavaVirtualMachine, + private val continuation: CancellableContinuation, + private val decoder: (JniEnvironment, Encoded) -> Decoded +) { + + private val stableRef = StableRef.create(this) + + var completed = false + + val address get() = stableRef.asCPointer().toLong() + + fun receiveFailure(env: JniEnvironment, error: jthrowable) { + completed = true + continuation.resumeWithException(env.processJvmException(error)) + stableRef.dispose() + } + + fun receiveSuccess(env: JniEnvironment, value: Encoded) { + completed = true + continuation.resume(decoder(env, value)) + stableRef.dispose() + } + + fun sendCancellation(invocation: jobject) { + // don't dispose here: wait for receiveSuccess or receiveFailure + jvm.useEnv { env -> env.callVoidMethod(invocation, cancelInvocationMethod) } + } +} + diff --git a/knee-runtime/src/backendMain/kotlin/module/KneeModule.kn.kt b/knee-runtime/src/backendMain/kotlin/module/KneeModule.kn.kt new file mode 100644 index 0000000..ef7bd2f --- /dev/null +++ b/knee-runtime/src/backendMain/kotlin/module/KneeModule.kn.kt @@ -0,0 +1,99 @@ +package io.deepmedia.tools.knee.runtime.module + +import io.deepmedia.tools.knee.runtime.JniEnvironment +import io.deepmedia.tools.knee.runtime.JniNativeMethod +import io.deepmedia.tools.knee.runtime.compiler.* +import io.deepmedia.tools.knee.runtime.javaVM +import io.deepmedia.tools.knee.runtime.registerNatives +import kotlinx.atomicfu.atomic + +class KneeModuleBuilder internal constructor() { + internal var initializer: ((JniEnvironment) -> Unit)? = null + internal val exportAdapters = mutableMapOf>() + + fun initialize(block: (JniEnvironment) -> Unit) { + initializer = block + } + + // export2, handled by plugin and replaced with exportAdapter() + inline fun export() { + error("export() error. Is Knee compiler plugin applied?") + } + + @PublishedApi + internal fun exportAdapter(typeId: Int, adapter: KneeModule.Adapter) { + exportAdapters[typeId] = adapter + } +} + +open class KneeModule @PublishedApi internal constructor( + private val registerNativeContainers: List, + private val registerNativeMethods: List>, + private val preloadFqns: List, + private val exceptions: List, + private val dependencies: List, + block: (KneeModuleBuilder.() -> Unit)? +) { + private val initializer: ((JniEnvironment) -> Unit)? + private val exportAdapters: Map> + init { + val builder = KneeModuleBuilder().apply { block?.invoke(this) } + initializer = builder.initializer + exportAdapters = builder.exportAdapters.toMap() + } + + @Suppress("unused") + constructor(vararg dependencies: KneeModule, block: (KneeModuleBuilder.() -> Unit)? = null) : this( + registerNativeContainers = emptyList(), + registerNativeMethods = emptyList(), + preloadFqns = emptyList(), + dependencies = dependencies.toList(), + exceptions = emptyList(), + block = block + ) + + // NOTE: could check types at runtime instead of UNCHECKED_CAST (pass them to Adapter constructor) + @Suppress("UNCHECKED_CAST") + internal fun getExportAdapter(typeId: Int): Adapter { + val adapter = checkNotNull(exportAdapters[typeId]) { "No adapter for type: $typeId" } + return adapter as Adapter + } + + internal fun collectExceptions(set: MutableSet) { + set.addAll(exceptions) + dependencies.forEach { it.collectExceptions(set) } + } + + private var initialized = atomic(0L) + + internal fun initializeIfNeeded(environment: JniEnvironment) { + val id = environment.javaVM.rawValue.toLong() + val oldId = initialized.getAndSet(id) + if (id != oldId) { + preloadFqns.forEach { + ClassIds.get(environment, it) + } + registerNativeContainers.forEachIndexed { index, classFqn -> + val methods = registerNativeMethods[index] + environment.registerNatives(classFqn, *methods.toTypedArray()) + } + initializer?.invoke(environment) + dependencies.forEach { + it.initializeIfNeeded(environment) + } + } + } + + @PublishedApi + internal class Adapter( + private val encoder: ((environment: JniEnvironment, decoded: Decoded) -> Encoded), + private val decoder: ((environment: JniEnvironment, encoded: Encoded) -> Decoded) + ) { + fun encode(environment: JniEnvironment, decoded: Decoded): Encoded { + return encoder(environment, decoded) + } + fun decode(environment: JniEnvironment, encoded: Encoded): Decoded { + return decoder(environment, encoded) + } + } +} diff --git a/knee-runtime/src/backendMain/kotlin/types/Booleans.kt b/knee-runtime/src/backendMain/kotlin/types/Booleans.kt new file mode 100644 index 0000000..dcc6221 --- /dev/null +++ b/knee-runtime/src/backendMain/kotlin/types/Booleans.kt @@ -0,0 +1,11 @@ +package io.deepmedia.tools.knee.runtime.types + +import platform.android.* + +@PublishedApi +internal fun decodeBoolean(data: jboolean): Boolean = data == JNI_TRUE.toUByte() + +@PublishedApi +internal fun encodeBoolean(data: Boolean): jboolean { + return if (data) JNI_TRUE.toUByte() else JNI_FALSE.toUByte() +} diff --git a/knee-runtime/src/backendMain/kotlin/types/Boxes.kt b/knee-runtime/src/backendMain/kotlin/types/Boxes.kt new file mode 100644 index 0000000..9052b3b --- /dev/null +++ b/knee-runtime/src/backendMain/kotlin/types/Boxes.kt @@ -0,0 +1,66 @@ +package io.deepmedia.tools.knee.runtime.types + +import io.deepmedia.tools.knee.runtime.* +import io.deepmedia.tools.knee.runtime.compiler.ClassIds +import io.deepmedia.tools.knee.runtime.compiler.BoxMethods +import platform.android.* + +@PublishedApi +internal fun encodeBoxedLong(env: JniEnvironment, input: jlong): jobject { + return env.newObject(ClassIds.get(env, "java.lang.Long"), BoxMethods.longConstructor, input) +} + +@PublishedApi +internal fun decodeBoxedLong(env: JniEnvironment, input: jobject): jlong { + return env.callLongMethod(input, BoxMethods.longValue) +} + +@PublishedApi +internal fun encodeBoxedInt(env: JniEnvironment, input: jint): jobject { + return env.newObject(ClassIds.get(env, "java.lang.Integer"), BoxMethods.intConstructor, input) +} + +@PublishedApi +internal fun decodeBoxedInt(env: JniEnvironment, input: jobject): jint { + return env.callIntMethod(input, BoxMethods.intValue) +} + +@PublishedApi +internal fun encodeBoxedByte(env: JniEnvironment, input: jbyte): jobject { + return env.newObject(ClassIds.get(env, "java.lang.Byte"), BoxMethods.byteConstructor, input) +} + +@PublishedApi +internal fun decodeBoxedByte(env: JniEnvironment, input: jobject): jbyte { + return env.callByteMethod(input, BoxMethods.byteValue) +} + +@PublishedApi +internal fun encodeBoxedBoolean(env: JniEnvironment, input: jboolean): jobject { + return env.newObject(ClassIds.get(env, "java.lang.Boolean"), BoxMethods.boolConstructor, input) +} + +@PublishedApi +internal fun decodeBoxedBoolean(env: JniEnvironment, input: jobject): jboolean { + return env.callBooleanMethod(input, BoxMethods.boolValue) +} + +@PublishedApi +internal fun encodeBoxedDouble(env: JniEnvironment, input: jdouble): jobject { + return env.newObject(ClassIds.get(env, "java.lang.Double"), BoxMethods.doubleConstructor, input) +} + +@PublishedApi +internal fun decodeBoxedDouble(env: JniEnvironment, input: jobject): jdouble { + return env.callDoubleMethod(input, BoxMethods.doubleValue) +} + +@PublishedApi +internal fun encodeBoxedFloat(env: JniEnvironment, input: jfloat): jobject { + return env.newObject(ClassIds.get(env, "java.lang.Float"), BoxMethods.floatConstructor, input) +} + +@PublishedApi +internal fun decodeBoxedFloat(env: JniEnvironment, input: jobject): jfloat { + return env.callFloatMethod(input, BoxMethods.floatValue) +} diff --git a/knee-runtime/src/backendMain/kotlin/types/Classes.kt b/knee-runtime/src/backendMain/kotlin/types/Classes.kt new file mode 100644 index 0000000..02d38e8 --- /dev/null +++ b/knee-runtime/src/backendMain/kotlin/types/Classes.kt @@ -0,0 +1,18 @@ +@file:Suppress("unused") + +package io.deepmedia.tools.knee.runtime.types + +import kotlinx.cinterop.* + +@PublishedApi +internal fun encodeClass(instance: Any): Long { + return StableRef.create(instance).asCPointer().toLong() +} + +@PublishedApi +internal inline fun decodeClass(instance: Long): T { + val pointer = checkNotNull(instance.toCPointer()) { + "Class reference $instance is invalid!" + } + return pointer.asStableRef().get() +} diff --git a/knee-runtime/src/backendMain/kotlin/types/Enums.kt b/knee-runtime/src/backendMain/kotlin/types/Enums.kt new file mode 100644 index 0000000..e5840c3 --- /dev/null +++ b/knee-runtime/src/backendMain/kotlin/types/Enums.kt @@ -0,0 +1,16 @@ +@file:Suppress("unused") + +package io.deepmedia.tools.knee.runtime.types + +import kotlin.enums.enumEntries + +@OptIn(ExperimentalStdlibApi::class) +@PublishedApi +internal inline fun > decodeEnum(index: Int): T { + return enumEntries()[index] +} + +@PublishedApi +internal fun > encodeEnum(instance: T): Int { + return instance.ordinal +} diff --git a/knee-runtime/src/backendMain/kotlin/types/Interfaces.kt b/knee-runtime/src/backendMain/kotlin/types/Interfaces.kt new file mode 100644 index 0000000..5a035ec --- /dev/null +++ b/knee-runtime/src/backendMain/kotlin/types/Interfaces.kt @@ -0,0 +1,38 @@ +package io.deepmedia.tools.knee.runtime.types + +import io.deepmedia.tools.knee.runtime.JniEnvironment +import io.deepmedia.tools.knee.runtime.compiler.ClassIds +import io.deepmedia.tools.knee.runtime.compiler.JvmInterfaceWrapper +import io.deepmedia.tools.knee.runtime.getObjectClass +import io.deepmedia.tools.knee.runtime.isSameObject +import kotlinx.cinterop.* +import platform.android.jlong +import platform.android.jobject + + +@Suppress("unused") +@PublishedApi +internal fun encodeInterface(environment: JniEnvironment, interface_: T): jobject { + return if (interface_ is JvmInterfaceWrapper<*>) { + interface_.jvmInterfaceObject + } else { + val address: jlong = StableRef.create(interface_).asCPointer().toLong() + encodeBoxedLong(environment, address) + } +} + +@Suppress("unused") +internal inline fun decodeInterface( + environment: JniEnvironment, + interface_: jobject, + wrapper: () -> JvmInterfaceWrapper +): T { + val interfaceClass = environment.getObjectClass(interface_) + val longClass = ClassIds.get(environment, "java.lang.Long") + return if (environment.isSameObject(interfaceClass, longClass)) { + val longValue = decodeBoxedLong(environment, interface_) + longValue.toCPointer()!!.asStableRef().get() + } else { + wrapper() as T + } +} \ No newline at end of file diff --git a/knee-runtime/src/backendMain/kotlin/types/Strings.kt b/knee-runtime/src/backendMain/kotlin/types/Strings.kt new file mode 100644 index 0000000..e7c6608 --- /dev/null +++ b/knee-runtime/src/backendMain/kotlin/types/Strings.kt @@ -0,0 +1,21 @@ +package io.deepmedia.tools.knee.runtime.types + +import io.deepmedia.tools.knee.runtime.* +import kotlinx.cinterop.* +import platform.android.* + +@PublishedApi +internal fun decodeString(env: JniEnvironment, data: jstring): String { + // The UTF8 version is null terminated so we can pass it to KN without reading length + // This is not the most efficient though + // https://developer.android.com/training/articles/perf-jni#utf-8-and-utf-16-strings + val chars = env.getStringUTFChars(data) + val str = chars.toKStringFromUtf8() + env.releaseStringUTFChars(data, chars) + return str +} + +@PublishedApi +internal fun encodeString(env: JniEnvironment, data: String): jstring { + return env.newStringUTF(data) +} \ No newline at end of file diff --git a/knee-runtime/src/frontendMain/kotlin/buffer/BufferTypeAliases.kt b/knee-runtime/src/frontendMain/kotlin/buffer/BufferTypeAliases.kt new file mode 100644 index 0000000..3408ab3 --- /dev/null +++ b/knee-runtime/src/frontendMain/kotlin/buffer/BufferTypeAliases.kt @@ -0,0 +1,7 @@ +package io.deepmedia.tools.knee.runtime.buffer + +typealias ByteBuffer = java.nio.ByteBuffer +typealias DoubleBuffer = java.nio.DoubleBuffer +typealias FloatBuffer = java.nio.FloatBuffer +typealias IntBuffer = java.nio.IntBuffer +typealias LongBuffer = java.nio.LongBuffer \ No newline at end of file diff --git a/knee-runtime/src/frontendMain/kotlin/compiler/Buffers.jvm.kt b/knee-runtime/src/frontendMain/kotlin/compiler/Buffers.jvm.kt new file mode 100644 index 0000000..82083d9 --- /dev/null +++ b/knee-runtime/src/frontendMain/kotlin/compiler/Buffers.jvm.kt @@ -0,0 +1,20 @@ +@file:JvmName("BuffersKt") +package io.deepmedia.tools.knee.runtime.compiler + +import java.nio.ByteBuffer +import java.nio.ByteOrder + +@Suppress("unused") +internal object KneeBuffers { + @JvmStatic + private fun setNativeOrder(buffer: ByteBuffer) { + buffer.order(ByteOrder.nativeOrder()) + } +} + + + + + + + diff --git a/knee-runtime/src/frontendMain/kotlin/compiler/Exceptions.jvm.kt b/knee-runtime/src/frontendMain/kotlin/compiler/Exceptions.jvm.kt new file mode 100644 index 0000000..6ed9f02 --- /dev/null +++ b/knee-runtime/src/frontendMain/kotlin/compiler/Exceptions.jvm.kt @@ -0,0 +1,27 @@ +@file:JvmName("ExceptionsKt") +package io.deepmedia.tools.knee.runtime.compiler + +import java.nio.ByteBuffer + +@Suppress("unused") +public class KneeKnExceptionToken(val reference: Long) : RuntimeException() { + protected fun finalize() { + clear(reference) + } + private external fun clear(reference: Long) + + private companion object { + @JvmStatic + @Suppress("unused") + private fun get(throwable: Throwable): Long { + val cause = throwable.cause as? KneeKnExceptionToken ?: return 0 + return cause.reference + } + } +} + + + + + + diff --git a/knee-runtime/src/frontendMain/kotlin/compiler/Instances.jvm.kt b/knee-runtime/src/frontendMain/kotlin/compiler/Instances.jvm.kt new file mode 100644 index 0000000..8d7c29d --- /dev/null +++ b/knee-runtime/src/frontendMain/kotlin/compiler/Instances.jvm.kt @@ -0,0 +1,38 @@ +@file:JvmName("InstancesKt") +@file:Suppress("RedundantVisibilityModifier") + +package io.deepmedia.tools.knee.runtime.compiler + +@Suppress("Unused") +public external fun kneeDisposeInstance(ref: Long) + +@Suppress("Unused") +public external fun kneeHashInstance(ref: Long): Int + +@Suppress("Unused") +public external fun kneeCompareInstance(ref0: Long, ref1: Long): Boolean + +@Suppress("Unused") +public external fun kneeDescribeInstance(ref: Long): String + +// Turns out we don't need these. We just act from JNI which has no access control + +/* @Suppress("Unused") +public fun kneeUnwrapInstance(instance: Any): Long { + return try { + val field = instance.javaClass.getDeclaredField("\$knee") + field.isAccessible = true + field.getLong(instance) + } catch (e: Throwable) { + 0L + } +} + +@Suppress("Unused") +public fun kneeWrapInstance(ref: Long, className: String): Any? { + return try { + Class.forName(className).getDeclaredConstructor(Long::class.java).newInstance(ref) + } catch (e: Throwable) { + null + } +} */ \ No newline at end of file diff --git a/knee-runtime/src/frontendMain/kotlin/compiler/Suspend.jvm.kt b/knee-runtime/src/frontendMain/kotlin/compiler/Suspend.jvm.kt new file mode 100644 index 0000000..a8eb6f4 --- /dev/null +++ b/knee-runtime/src/frontendMain/kotlin/compiler/Suspend.jvm.kt @@ -0,0 +1,90 @@ +@file:JvmName("SuspendKt") +package io.deepmedia.tools.knee.runtime.compiler + +import kotlinx.coroutines.* +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.concurrent.Volatile +import kotlin.coroutines.cancellation.CancellationException +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +@Suppress("UNCHECKED_CAST") +public class KneeSuspendInvoker(private val continuation: CancellableContinuation) { + + private val completed = AtomicBoolean(false) + + @Suppress("unused") + fun receiveSuccess(result: Any?) { + if (completed.compareAndSet(false, true)) { + continuation.resume(result as RawResultType) + } + } + + @Suppress("unused") + fun receiveFailure(exception: Throwable) { + if (completed.compareAndSet(false, true)) { + continuation.resumeWithException(exception) + } + } + + // @PublishedApi is needed otherwise function gets $knee_runtime suffix + // Looks like a Kotlin bug + @PublishedApi + internal external fun sendCancellation(invocation: Long) + + @PublishedApi + internal fun sendCancellationSafe(invocation: Long) { + if (completed.compareAndSet(false, true)) { + sendCancellation(invocation) + } + } +} + +@Suppress("unused") +public suspend inline fun kneeInvokeJvmSuspend(crossinline block: (KneeSuspendInvoker) -> Long): RawResultType { + return suspendCancellableCoroutine { cont -> + val invoker = KneeSuspendInvoker(cont) + val invocation: Long = block(invoker) + cont.invokeOnCancellation { + invoker.sendCancellationSafe(invocation) + } + } +} + +// REVERSE SUSPEND SUPPORT + +private val KneeScope = CoroutineScope(Dispatchers.Default + CoroutineName("Knee")) + +// called by codegen JVM code +@Suppress("unused") +public fun kneeInvokeKnSuspend(invoker: Long, block: suspend () -> T): KneeSuspendInvocation { + return KneeSuspendInvocation(invoker, block) +} + +// T = encoded result +public class KneeSuspendInvocation( + private val invoker: Long, + block: suspend () -> T +) { + private val job = KneeScope.launch(start = CoroutineStart.UNDISPATCHED) { + try { + val result = block() + sendSuccess(invoker, result) + } catch (e: Throwable) { + sendFailure(invoker, e) + } + } + + private external fun sendFailure(invoker: Long, error: Throwable) + + private external fun sendSuccess(invoker: Long, value: Any?) + + // Called from K/N + @Suppress("unused") + private fun receiveCancellation() { + job.cancel() + } +} + + + diff --git a/knee-runtime/src/frontendMain/kotlin/module/KneeModule.jvm.kt b/knee-runtime/src/frontendMain/kotlin/module/KneeModule.jvm.kt new file mode 100644 index 0000000..81b8166 --- /dev/null +++ b/knee-runtime/src/frontendMain/kotlin/module/KneeModule.jvm.kt @@ -0,0 +1,22 @@ +package io.deepmedia.tools.knee.runtime.module + + +@Suppress("unused") +abstract class KneeModule { + + protected abstract val exportAdapters: Map> + + @Suppress("UNCHECKED_CAST") + fun getExportAdapter(typeId: Int): Adapter { + val adapter = checkNotNull(exportAdapters[typeId]) { "No adapter for type: $typeId" } + return adapter as Adapter + } + + class Adapter( + private val encoder: (decoded: Decoded) -> Encoded, + private val decoder: (encoded: Encoded) -> Decoded + ) { + fun encode(decoded: Decoded): Encoded = encoder(decoded) + fun decode(encoded: Encoded): Decoded = decoder(encoded) + } +} \ No newline at end of file diff --git a/knee-runtime/src/prebuiltHeadersMain/interop/jni_prebuilt.def b/knee-runtime/src/prebuiltHeadersMain/interop/jni_prebuilt.def new file mode 100644 index 0000000..7b5e9cd --- /dev/null +++ b/knee-runtime/src/prebuiltHeadersMain/interop/jni_prebuilt.def @@ -0,0 +1,2 @@ +headers = jni.h +headerFilter = jni.h jni_md.h \ No newline at end of file diff --git a/knee-runtime/src/prebuiltHeadersMain/kotlin/JniDefinitions.prebuilt.kt b/knee-runtime/src/prebuiltHeadersMain/kotlin/JniDefinitions.prebuilt.kt new file mode 100644 index 0000000..197d2f5 --- /dev/null +++ b/knee-runtime/src/prebuiltHeadersMain/kotlin/JniDefinitions.prebuilt.kt @@ -0,0 +1,6 @@ +package io.deepmedia.tools.knee.runtime + +// actual val JNI_OK: Int get() = io.deepmedia.tools.knee.runtime.internal.JNI_OK +// actual val JNI_VERSION_1_6: Int get() = io.deepmedia.tools.knee.runtime.internal.JNI_VERSION_1_6 +// actual typealias JNIInvokeInterface = io.deepmedia.tools.knee.runtime.internal.JNIInvokeInterface_ +// actual typealias JNINativeInterface = io.deepmedia.tools.knee.runtime.internal.JNINativeInterface_ diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..822d175 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,46 @@ +enableFeaturePreview("STABLE_CONFIGURATION_CACHE") + +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + google() + mavenLocal() + + val props = java.util.Properties().apply { + file("local.properties") + .takeIf { it.exists() } + ?.inputStream() + ?.use { load(it) } + } + val user: String? = "GHUB_USER".let { props.getProperty(it) ?: System.getenv(it) } + val token: String? = "GHUB_PERSONAL_ACCESS_TOKEN".let { props.getProperty(it) ?: System.getenv(it) } + if (!user.isNullOrEmpty() && !token.isNullOrEmpty()) { + maven { + url = uri("https://maven.pkg.github.com/deepmedia/MavenDeployer") + credentials.username = user + credentials.password = token + } + } + } + + plugins { + kotlin("multiplatform") version "2.0.0" apply false + kotlin("plugin.serialization") version "2.0.0" apply false + kotlin("jvm") version "2.0.0" apply false + id("io.deepmedia.tools.deployer") version "0.11.0-rc01" apply false + } +} + +dependencyResolutionManagement { + repositories { + mavenCentral() + mavenLocal() + google() + } +} + +include(":knee-annotations") +include(":knee-runtime") +include(":knee-compiler-plugin") +include(":knee-gradle-plugin") diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 0000000..603b140 --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1,14 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx diff --git a/tests/.idea/.gitignore b/tests/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/tests/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/tests/.idea/.name b/tests/.idea/.name new file mode 100644 index 0000000..e9658e5 --- /dev/null +++ b/tests/.idea/.name @@ -0,0 +1 @@ +KneeTests \ No newline at end of file diff --git a/tests/.idea/androidTestResultsUserPreferences.xml b/tests/.idea/androidTestResultsUserPreferences.xml new file mode 100644 index 0000000..ed223f8 --- /dev/null +++ b/tests/.idea/androidTestResultsUserPreferences.xml @@ -0,0 +1,89 @@ + + + + + + \ No newline at end of file diff --git a/tests/.idea/artifacts/knee_annotations_frontend_0_2_0_SNAPSHOT.xml b/tests/.idea/artifacts/knee_annotations_frontend_0_2_0_SNAPSHOT.xml new file mode 100644 index 0000000..52929e5 --- /dev/null +++ b/tests/.idea/artifacts/knee_annotations_frontend_0_2_0_SNAPSHOT.xml @@ -0,0 +1,8 @@ + + + $PROJECT_DIR$/../knee-annotations/build/libs + + + + + \ No newline at end of file diff --git a/tests/.idea/artifacts/knee_annotations_frontend_0_3_0_SNAPSHOT.xml b/tests/.idea/artifacts/knee_annotations_frontend_0_3_0_SNAPSHOT.xml new file mode 100644 index 0000000..317384a --- /dev/null +++ b/tests/.idea/artifacts/knee_annotations_frontend_0_3_0_SNAPSHOT.xml @@ -0,0 +1,8 @@ + + + $PROJECT_DIR$/../knee-annotations/build/libs + + + + + \ No newline at end of file diff --git a/tests/.idea/artifacts/knee_runtime_frontend_0_2_0_SNAPSHOT.xml b/tests/.idea/artifacts/knee_runtime_frontend_0_2_0_SNAPSHOT.xml new file mode 100644 index 0000000..f2c5d88 --- /dev/null +++ b/tests/.idea/artifacts/knee_runtime_frontend_0_2_0_SNAPSHOT.xml @@ -0,0 +1,8 @@ + + + $PROJECT_DIR$/../knee-runtime/build/libs + + + + + \ No newline at end of file diff --git a/tests/.idea/artifacts/knee_runtime_frontend_0_3_0_SNAPSHOT.xml b/tests/.idea/artifacts/knee_runtime_frontend_0_3_0_SNAPSHOT.xml new file mode 100644 index 0000000..83bdd6f --- /dev/null +++ b/tests/.idea/artifacts/knee_runtime_frontend_0_3_0_SNAPSHOT.xml @@ -0,0 +1,8 @@ + + + $PROJECT_DIR$/../knee-runtime/build/libs + + + + + \ No newline at end of file diff --git a/tests/.idea/compiler.xml b/tests/.idea/compiler.xml new file mode 100644 index 0000000..fb7f4a8 --- /dev/null +++ b/tests/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/tests/.idea/deploymentTargetDropDown.xml b/tests/.idea/deploymentTargetDropDown.xml new file mode 100644 index 0000000..bf2abeb --- /dev/null +++ b/tests/.idea/deploymentTargetDropDown.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/.idea/gradle.xml b/tests/.idea/gradle.xml new file mode 100644 index 0000000..1b0c679 --- /dev/null +++ b/tests/.idea/gradle.xml @@ -0,0 +1,43 @@ + + + + + + + \ No newline at end of file diff --git a/tests/.idea/kotlinc.xml b/tests/.idea/kotlinc.xml new file mode 100644 index 0000000..6d0ee1c --- /dev/null +++ b/tests/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/tests/.idea/misc.xml b/tests/.idea/misc.xml new file mode 100644 index 0000000..eca22b7 --- /dev/null +++ b/tests/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/tests/.idea/runConfigurations/link_imports.xml b/tests/.idea/runConfigurations/link_imports.xml new file mode 100644 index 0000000..a415683 --- /dev/null +++ b/tests/.idea/runConfigurations/link_imports.xml @@ -0,0 +1,24 @@ + + + + + + + true + true + false + false + + + \ No newline at end of file diff --git a/tests/.idea/runConfigurations/link_misc.xml b/tests/.idea/runConfigurations/link_misc.xml new file mode 100644 index 0000000..5525357 --- /dev/null +++ b/tests/.idea/runConfigurations/link_misc.xml @@ -0,0 +1,24 @@ + + + + + + + true + true + false + false + + + \ No newline at end of file diff --git a/tests/.idea/runConfigurations/link_primitives.xml b/tests/.idea/runConfigurations/link_primitives.xml new file mode 100644 index 0000000..956732e --- /dev/null +++ b/tests/.idea/runConfigurations/link_primitives.xml @@ -0,0 +1,24 @@ + + + + + + + true + true + false + false + + + \ No newline at end of file diff --git a/tests/.idea/runConfigurations/test_classes.xml b/tests/.idea/runConfigurations/test_classes.xml new file mode 100644 index 0000000..bd1316d --- /dev/null +++ b/tests/.idea/runConfigurations/test_classes.xml @@ -0,0 +1,32 @@ + + + + + \ No newline at end of file diff --git a/tests/.idea/runConfigurations/test_coroutines.xml b/tests/.idea/runConfigurations/test_coroutines.xml new file mode 100644 index 0000000..cb69347 --- /dev/null +++ b/tests/.idea/runConfigurations/test_coroutines.xml @@ -0,0 +1,32 @@ + + + + + \ No newline at end of file diff --git a/tests/.idea/runConfigurations/test_imports.xml b/tests/.idea/runConfigurations/test_imports.xml new file mode 100644 index 0000000..e258cf3 --- /dev/null +++ b/tests/.idea/runConfigurations/test_imports.xml @@ -0,0 +1,32 @@ + + + + + \ No newline at end of file diff --git a/tests/.idea/runConfigurations/test_interfaces.xml b/tests/.idea/runConfigurations/test_interfaces.xml new file mode 100644 index 0000000..336af4f --- /dev/null +++ b/tests/.idea/runConfigurations/test_interfaces.xml @@ -0,0 +1,35 @@ + + + + + \ No newline at end of file diff --git a/tests/.idea/runConfigurations/test_misc.xml b/tests/.idea/runConfigurations/test_misc.xml new file mode 100644 index 0000000..532ee5f --- /dev/null +++ b/tests/.idea/runConfigurations/test_misc.xml @@ -0,0 +1,32 @@ + + + + + \ No newline at end of file diff --git a/tests/.idea/runConfigurations/test_primitives.xml b/tests/.idea/runConfigurations/test_primitives.xml new file mode 100644 index 0000000..8f83b72 --- /dev/null +++ b/tests/.idea/runConfigurations/test_primitives.xml @@ -0,0 +1,32 @@ + + + + + \ No newline at end of file diff --git a/tests/.idea/uiDesigner.xml b/tests/.idea/uiDesigner.xml new file mode 100644 index 0000000..2b63946 --- /dev/null +++ b/tests/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/.idea/vcs.xml b/tests/.idea/vcs.xml new file mode 100644 index 0000000..bcdd6b9 --- /dev/null +++ b/tests/.idea/vcs.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/tests/gradle.properties b/tests/gradle.properties new file mode 100644 index 0000000..85e2d08 --- /dev/null +++ b/tests/gradle.properties @@ -0,0 +1,9 @@ +kotlin.mpp.stability.nowarn=true +android.useAndroidX=true +org.gradle.caching=true +kotlin.incremental.useClasspathSnapshot=true +kotlin.mpp.import.enableKgpDependencyResolution=true +org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m + +io.deepmedia.knee.verboseLogs=true +io.deepmedia.knee.verboseSources=true diff --git a/tests/gradle/wrapper/gradle-wrapper.jar b/tests/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..7454180 Binary files /dev/null and b/tests/gradle/wrapper/gradle-wrapper.jar differ diff --git a/tests/gradle/wrapper/gradle-wrapper.properties b/tests/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..27313fb --- /dev/null +++ b/tests/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/tests/gradlew b/tests/gradlew new file mode 100755 index 0000000..744e882 --- /dev/null +++ b/tests/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or 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 +# +# https://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. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MSYS* | MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/tests/gradlew.bat b/tests/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/tests/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/tests/settings.gradle.kts b/tests/settings.gradle.kts new file mode 100644 index 0000000..ef2e64e --- /dev/null +++ b/tests/settings.gradle.kts @@ -0,0 +1,29 @@ +pluginManagement { + repositories { + mavenCentral() + gradlePluginPortal() + google() + mavenLocal() + } +} + +enableFeaturePreview("STABLE_CONFIGURATION_CACHE") + +dependencyResolutionManagement { + repositories { + mavenCentral() + mavenLocal() + google() + } +} + +includeBuild("..") + +include("test-coroutines") +include("test-interfaces") +include("test-primitives") +include("test-imports") +include("test-classes") +include("test-misc") + +rootProject.name = "KneeTests" \ No newline at end of file diff --git a/tests/test-classes/.gitignore b/tests/test-classes/.gitignore new file mode 100644 index 0000000..f3d6549 --- /dev/null +++ b/tests/test-classes/.gitignore @@ -0,0 +1 @@ +/build/ \ No newline at end of file diff --git a/tests/test-classes/build.gradle.kts b/tests/test-classes/build.gradle.kts new file mode 100644 index 0000000..049a51a --- /dev/null +++ b/tests/test-classes/build.gradle.kts @@ -0,0 +1,65 @@ +@file:Suppress("UnstableApiUsage") + +plugins { + kotlin("multiplatform") version "2.0.0" + id("com.android.application") version "8.1.1" + id("io.deepmedia.tools.knee") version "0.3.0-SNAPSHOT" +} + +configurations.configureEach { + resolutionStrategy { + cacheChangingModulesFor(0, "seconds") + } +} + +android { + namespace = "io.deepmedia.tools.knee.tests" + compileSdk = 34 + defaultConfig { + minSdk = 26 + targetSdk = 34 + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } +} + +dependencies { + androidTestImplementation("androidx.test:runner:1.5.2") + androidTestImplementation("androidx.test:rules:1.5.0") +} + +knee { + +} + +kotlin { + jvmToolchain(11) + + applyDefaultHierarchyTemplate { + common { + group("backend") { + withAndroidNative() + } + } + } + + // frontend + androidTarget() + + // backend + androidNativeArm64() + androidNativeX64() + androidNativeArm32() + androidNativeX86() +} + +/** + * This is to make included parent build work. Kotlin Compiler Plugins have a configuration + * issue: https://youtrack.jetbrains.com/issue/KT-53477/ . We workaround this in kotlin-compiler-plugin + * by using a fat JAR, but this fat JAR is exported from the "shadow" configuration, while included builds + * read from the "default" configuration and there doesn't seem to be a clean way to solve this. + */ +configurations.matching { + it.name.startsWith("kotlin") && it.name.contains("CompilerPluginClasspath") +}.all { + isTransitive = true +} \ No newline at end of file diff --git a/tests/test-classes/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/ClassTests.kt b/tests/test-classes/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/ClassTests.kt new file mode 100644 index 0000000..c64f233 --- /dev/null +++ b/tests/test-classes/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/ClassTests.kt @@ -0,0 +1,74 @@ +package io.deepmedia.tools.knee.tests + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import org.junit.Test + +// TODO: test garbage collection +class ClassTests { + + companion object { + init { + System.loadLibrary("test_classes") + } + } + + @Test + fun testProperty() { + val item = Counter() + currentCounter = item + check(item == currentCounter) + check(item !== currentCounter) + } + + @Test + fun testEquals() { + // Counter implements equals based on the value + val first = Counter(initialValue = 50u) + val second = Counter(initialValue = 100u) + val third = Counter(initialValue = 100u) + check(first != second) + check(second == third) + } + + @Test + fun testString() { + val item = Counter(initialValue = 0u) + check(item.toString() == "Counter(0)") + item.increment() + check(item.toString() == "Counter(1)") + } + + @Test + fun testHashCode() { + val item = Counter(initialValue = 30u) + check(30 == item.hashCode()) + } + + @Test + fun testSameNativeObject() { + val item = Counter() + currentCounter = item + check(isCurrentCounter(item, checkIdentity = false)) + check(isCurrentCounter(item, checkIdentity = true)) + } + + @Test + fun testArray() { + val array = arrayOfCounters(4) + check(array.size == 4) + } + + @Test + fun testMutability() { + currentCounter = Counter(10u) + currentCounter!!.increment() + check(currentCounter!!.get() == 11u) + } + + @Test + fun testFlip() { + val item = Counter() + check(item.flip(false)) + } +} diff --git a/tests/test-classes/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/InnerClassTests.kt b/tests/test-classes/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/InnerClassTests.kt new file mode 100644 index 0000000..c3eba5f --- /dev/null +++ b/tests/test-classes/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/InnerClassTests.kt @@ -0,0 +1,60 @@ +package io.deepmedia.tools.knee.tests + +import org.junit.Test + +class InnerClassTests { + + companion object { + init { + System.loadLibrary("test_classes") + } + } + + + @Test + fun testProperty() { + val item = Outer.InnerCounter() + currentInnerCounter = item + check(item == currentInnerCounter) + check(item !== currentInnerCounter) + } + + @Test + fun testEquals() { + // Counter implements equals based on the value + val first = Outer.InnerCounter(initialValue = 50u) + val second = Outer.InnerCounter(initialValue = 100u) + val third = Outer.InnerCounter(initialValue = 100u) + check(first != second) + check(second == third) + } + + @Test + fun testString() { + val item = Outer.InnerCounter(initialValue = 0u) + check(item.toString() == "Outer.InnerCounter(0)") + item.increment() + check(item.toString() == "Outer.InnerCounter(1)") + } + + @Test + fun testHashCode() { + val item = Outer.InnerCounter(initialValue = 30u) + check(30 == item.hashCode()) + } + + @Test + fun testSameNativeObject() { + val item = Outer.InnerCounter() + currentInnerCounter = item + check(isCurrentInnerCounter(item, checkIdentity = false)) + check(isCurrentInnerCounter(item, checkIdentity = true)) + } + + @Test + fun testMutability() { + currentInnerCounter = Outer.InnerCounter(10u) + currentInnerCounter!!.increment() + check(currentInnerCounter!!.get() == 11u) + } +} diff --git a/tests/test-classes/src/androidMain/AndroidManifest.xml b/tests/test-classes/src/androidMain/AndroidManifest.xml new file mode 100644 index 0000000..a4ce17d --- /dev/null +++ b/tests/test-classes/src/androidMain/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/tests/test-classes/src/backendMain/kotlin/Definintions.kt b/tests/test-classes/src/backendMain/kotlin/Definintions.kt new file mode 100644 index 0000000..5245317 --- /dev/null +++ b/tests/test-classes/src/backendMain/kotlin/Definintions.kt @@ -0,0 +1,61 @@ +package io.deepmedia.tools.knee.tests + +import io.deepmedia.tools.knee.annotations.* +import io.deepmedia.tools.knee.runtime.* +import kotlin.native.identityHashCode +import kotlin.random.Random +import kotlin.random.nextUInt + +@KneeClass +class Counter @Knee constructor(initialValue: UInt) { + var value: UInt = initialValue + + @Knee constructor() : this(initialValue = Random.nextUInt()) + @Knee fun increment() { value += 1u } + @Knee fun decrement() { value -= 1u } + @Knee fun add(delta: UInt) { value += delta } + @Knee fun get(): UInt = value + @Knee fun flip(bool: Boolean): Boolean = !bool + override fun toString(): String = "Counter($value)" + override fun hashCode(): Int = value.toInt() + override fun equals(other: Any?): Boolean { + return other is Counter && other.value == value + } +} + +@Knee var currentCounter: Counter? = null + +@Knee fun isCurrentCounter(counter: Counter, checkIdentity: Boolean): Boolean { + if (checkIdentity) return counter === currentCounter + return counter == currentCounter +} + +@Knee fun arrayOfCounters(size: Int): Array { + return Array(size) { Counter() } +} + +interface Outer { + + @KneeClass + class InnerCounter @Knee constructor(initialValue: UInt) { + var value: UInt = initialValue + + @Knee constructor() : this(initialValue = Random.nextUInt()) + @Knee fun increment() { value += 1u } + @Knee fun decrement() { value -= 1u } + @Knee fun add(delta: UInt) { value += delta } + @Knee fun get(): UInt = value + override fun toString(): String = "Outer.InnerCounter($value)" + override fun hashCode(): Int = value.toInt() + override fun equals(other: Any?): Boolean { + return other is InnerCounter && other.value == value + } + } +} + +@Knee var currentInnerCounter: Outer.InnerCounter? = null + +@Knee fun isCurrentInnerCounter(counter: Outer.InnerCounter, checkIdentity: Boolean): Boolean { + if (checkIdentity) return counter === currentInnerCounter + return counter == currentInnerCounter +} \ No newline at end of file diff --git a/tests/test-classes/src/backendMain/kotlin/Init.kt b/tests/test-classes/src/backendMain/kotlin/Init.kt new file mode 100644 index 0000000..f285264 --- /dev/null +++ b/tests/test-classes/src/backendMain/kotlin/Init.kt @@ -0,0 +1,17 @@ +package io.deepmedia.tools.knee.tests + +import io.deepmedia.tools.knee.annotations.* +import io.deepmedia.tools.knee.runtime.JavaVirtualMachine +import io.deepmedia.tools.knee.runtime.attachCurrentThread +import io.deepmedia.tools.knee.runtime.module.KneeModule +import io.deepmedia.tools.knee.runtime.useEnv +import kotlinx.cinterop.ExperimentalForeignApi +import kotlin.experimental.ExperimentalNativeApi + + +@OptIn(ExperimentalForeignApi::class, ExperimentalNativeApi::class) +@CName(externName = "JNI_OnLoad") +fun onLoad(vm: JavaVirtualMachine): Int { + vm.useEnv { io.deepmedia.tools.knee.runtime.initKnee(it) } + return 0x00010006 +} diff --git a/tests/test-coroutines/.gitignore b/tests/test-coroutines/.gitignore new file mode 100644 index 0000000..f3d6549 --- /dev/null +++ b/tests/test-coroutines/.gitignore @@ -0,0 +1 @@ +/build/ \ No newline at end of file diff --git a/tests/test-coroutines/build.gradle.kts b/tests/test-coroutines/build.gradle.kts new file mode 100644 index 0000000..92f7b3d --- /dev/null +++ b/tests/test-coroutines/build.gradle.kts @@ -0,0 +1,65 @@ +@file:Suppress("UnstableApiUsage") + +plugins { + kotlin("multiplatform") version "2.0.0" + id("com.android.application") version "8.1.1" + id("io.deepmedia.tools.knee") version "0.3.0-SNAPSHOT" +} + +configurations.configureEach { + resolutionStrategy { + cacheChangingModulesFor(0, "seconds") + } +} + +android { + namespace = "io.deepmedia.tools.knee.tests" + compileSdk = 34 + defaultConfig { + minSdk = 26 + targetSdk = 34 + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } +} + +dependencies { + androidTestImplementation("androidx.test:runner:1.5.2") + androidTestImplementation("androidx.test:rules:1.5.0") +} + +knee { + +} + +kotlin { + jvmToolchain(11) + + // frontend + androidTarget() + + // backend + applyDefaultHierarchyTemplate { + common { + group("backend") { + withAndroidNative() + } + } + } + + androidNativeArm64() + androidNativeX64() + androidNativeArm32() + androidNativeX86() +} + +/** + * This is to make included parent build work. Kotlin Compiler Plugins have a configuration + * issue: https://youtrack.jetbrains.com/issue/KT-53477/ . We workaround this in kotlin-compiler-plugin + * by using a fat JAR, but this fat JAR is exported from the "shadow" configuration, while included builds + * read from the "default" configuration and there doesn't seem to be a clean way to solve this. + */ +configurations.matching { + it.name.startsWith("kotlin") && it.name.contains("CompilerPluginClasspath") +}.all { + isTransitive = true +} \ No newline at end of file diff --git a/tests/test-coroutines/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/ReverseSuspendTests.kt b/tests/test-coroutines/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/ReverseSuspendTests.kt new file mode 100644 index 0000000..96aa515 --- /dev/null +++ b/tests/test-coroutines/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/ReverseSuspendTests.kt @@ -0,0 +1,80 @@ +package io.deepmedia.tools.knee.tests + +import kotlinx.coroutines.* +import org.junit.Test + +class ReverseSuspendTests { + + companion object { + init { + System.loadLibrary("test_coroutines") + } + } + + val util = object : ReverseUtil { + override suspend fun sumInts(first: Int, second: Int, delay: Long): Int { + if (delay > 0) delay(delay) + return first + second + } + override suspend fun sumLists(first: List, second: List, delay: Long): List { + if (delay > 0) delay(delay) + return first + second + } + override suspend fun sumStrings(first: String, second: String, delay: Long): String { + if (delay > 0) delay(delay) + return first + second + } + override suspend fun sumNullableStrings(first: String?, second: String?, delay: Long): String? { + if (delay > 0) delay(delay) + if (first == null && second == null) return null + return "$first$second" + } + override suspend fun crash(message: String, delay: Long) { + if (delay > 0) delay(delay) + error(message) + } + } + + @Test + fun testSuspend() = runBlocking { + check(30 == invokeSumInts(util, 10, 20, 0)) + } + + @Test + fun testSuspendWithDelay() = runBlocking { + check(30 == invokeSumInts(util, 10, 20, 1000)) + } + + // Use more complex types + @Test + fun testSuspendStrings() = runBlocking { + check("Hello, world!" == invokeSumStrings(util, "Hello, ", "world!", 100)) + } + @Test + fun testSuspendNullableStrings() = runBlocking { + check(null == invokeSumNullableStrings(util, null, null, 50)) + check("foo:null" == invokeSumNullableStrings(util, "foo:", null, 50)) + check("null:foo" == invokeSumNullableStrings(util, null, ":foo", 50)) + check("foo:foo" == invokeSumNullableStrings(util, "foo:", "foo", 50)) + } + + @Test + fun testSuspendLists() = runBlocking { + check(listOf(0, 1, 2, 3) == invokeSumLists(util, listOf(0, 1), listOf(2, 3), 100)) + } + + @Test + fun testCancellation() = runBlocking(Dispatchers.Default) { + val res = withTimeoutOrNull(1000) { + invokeSumInts(util, 0, 0, 2000) + } + check(res == null) + } + + @Test + fun testFailure() = runBlocking { + val e = runCatching { invokeCrash(util, "!!!", 100) }.exceptionOrNull() + check(e?.message != null && e.message!!.contains("!!!")) + } + +} diff --git a/tests/test-coroutines/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/SuspendTests.kt b/tests/test-coroutines/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/SuspendTests.kt new file mode 100644 index 0000000..860959b --- /dev/null +++ b/tests/test-coroutines/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/SuspendTests.kt @@ -0,0 +1,56 @@ +package io.deepmedia.tools.knee.tests + +import kotlinx.coroutines.* +import org.junit.Test + +class SuspendTests { + + companion object { + init { + System.loadLibrary("test_coroutines") + } + } + + @Test + fun testSuspend() = runBlocking { + check(30 == sumInts(10, 20, 0)) + } + + @Test + fun testSuspendWithDelay() = runBlocking { + check(30 == sumInts(10, 20, 1000)) + } + + // Use more complex types + @Test + fun testSuspendStrings() = runBlocking { + check("Hello, world!" == sumStrings("Hello, ", "world!", 100)) + } + @Test + fun testSuspendNullableStrings() = runBlocking { + check(null == sumNullableStrings(null, null, 50)) + check("foo:null" == sumNullableStrings("foo:", null, 50)) + check("null:foo" == sumNullableStrings(null, ":foo", 50)) + check("foo:foo" == sumNullableStrings("foo:", "foo", 50)) + } + + @Test + fun testSuspendLists() = runBlocking { + check(listOf(0, 1, 2, 3) == sumLists(listOf(0, 1), listOf(2, 3), 100)) + } + + @Test + fun testCancellation() = runBlocking(Dispatchers.Default) { + val res = withTimeoutOrNull(1000) { + sumInts(0, 0, 2000) + } + check(res == null) + } + + @Test + fun testFailure() = runBlocking { + val e = runCatching { crash("!!!", 100) }.exceptionOrNull() + check(e?.message != null && e.message!!.contains("!!!")) + } + +} diff --git a/tests/test-coroutines/src/androidMain/AndroidManifest.xml b/tests/test-coroutines/src/androidMain/AndroidManifest.xml new file mode 100644 index 0000000..a4ce17d --- /dev/null +++ b/tests/test-coroutines/src/androidMain/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/tests/test-coroutines/src/backendMain/kotlin/Init.kt b/tests/test-coroutines/src/backendMain/kotlin/Init.kt new file mode 100644 index 0000000..4e5b9a7 --- /dev/null +++ b/tests/test-coroutines/src/backendMain/kotlin/Init.kt @@ -0,0 +1,13 @@ +package io.deepmedia.tools.knee.tests + +import io.deepmedia.tools.knee.runtime.JavaVirtualMachine +import io.deepmedia.tools.knee.runtime.useEnv +import kotlinx.cinterop.ExperimentalForeignApi +import kotlin.experimental.ExperimentalNativeApi + +@OptIn(ExperimentalForeignApi::class, ExperimentalNativeApi::class) +@CName(externName = "JNI_OnLoad") +fun onLoad(vm: JavaVirtualMachine): Int { + vm.useEnv { io.deepmedia.tools.knee.runtime.initKnee(it) } + return 0x00010006 +} diff --git a/tests/test-coroutines/src/backendMain/kotlin/ReverseSuspendDefinitions.kt b/tests/test-coroutines/src/backendMain/kotlin/ReverseSuspendDefinitions.kt new file mode 100644 index 0000000..248852a --- /dev/null +++ b/tests/test-coroutines/src/backendMain/kotlin/ReverseSuspendDefinitions.kt @@ -0,0 +1,40 @@ +package io.deepmedia.tools.knee.tests + +import io.deepmedia.tools.knee.annotations.* +import io.deepmedia.tools.knee.runtime.* +import kotlinx.coroutines.delay + +@KneeInterface +interface ReverseUtil { + suspend fun sumInts(first: Int, second: Int, delay: Long): Int + suspend fun sumStrings(first: String, second: String, delay: Long): String + suspend fun sumNullableStrings(first: String?, second: String?, delay: Long): String? + suspend fun sumLists(first: List, second: List, delay: Long): List + suspend fun crash(message: String, delay: Long): Unit +} + +@Knee +suspend fun invokeSumInts(receiver: ReverseUtil, first: Int, second: Int, delay: Long): Int { + return receiver.sumInts(first, second, delay) +} + +@Knee +suspend fun invokeSumStrings(receiver: ReverseUtil, first: String, second: String, delay: Long): String { + return receiver.sumStrings(first, second, delay) +} + +@Knee +suspend fun invokeSumNullableStrings(receiver: ReverseUtil, first: String?, second: String?, delay: Long): String? { + return receiver.sumNullableStrings(first, second, delay) +} + +@Knee +suspend fun invokeSumLists(receiver: ReverseUtil, first: List, second: List, delay: Long): List { + return receiver.sumLists(first, second, delay) +} + +@Knee +suspend fun invokeCrash(receiver: ReverseUtil, message: String, delay: Long) { + return receiver.crash(message, delay) +} + diff --git a/tests/test-coroutines/src/backendMain/kotlin/SuspendDefinitions.kt b/tests/test-coroutines/src/backendMain/kotlin/SuspendDefinitions.kt new file mode 100644 index 0000000..aa0cb22 --- /dev/null +++ b/tests/test-coroutines/src/backendMain/kotlin/SuspendDefinitions.kt @@ -0,0 +1,37 @@ +package io.deepmedia.tools.knee.tests + +import io.deepmedia.tools.knee.annotations.* +import io.deepmedia.tools.knee.runtime.* +import kotlinx.coroutines.delay + + +@Knee +suspend fun sumInts(first: Int, second: Int, delay: Long): Int { + if (delay > 0) kotlinx.coroutines.delay(delay) + return first + second +} + +@Knee +suspend fun sumStrings(first: String, second: String, delay: Long): String { + if (delay > 0) kotlinx.coroutines.delay(delay) + return first + second +} + +@Knee +suspend fun sumLists(first: List, second: List, delay: Long): List { + if (delay > 0) kotlinx.coroutines.delay(delay) + return first + second +} + +@Knee +suspend fun sumNullableStrings(first: String?, second: String?, delay: Long): String? { + if (delay > 0) kotlinx.coroutines.delay(delay) + if (first == null && second == null) return null + return "$first$second" +} + +@Knee +suspend fun crash(message: String, delay: Long): Unit { + if (delay > 0) kotlinx.coroutines.delay(delay) + error(message) +} \ No newline at end of file diff --git a/tests/test-imports/.gitignore b/tests/test-imports/.gitignore new file mode 100644 index 0000000..f3d6549 --- /dev/null +++ b/tests/test-imports/.gitignore @@ -0,0 +1 @@ +/build/ \ No newline at end of file diff --git a/tests/test-imports/build.gradle.kts b/tests/test-imports/build.gradle.kts new file mode 100644 index 0000000..92f7b3d --- /dev/null +++ b/tests/test-imports/build.gradle.kts @@ -0,0 +1,65 @@ +@file:Suppress("UnstableApiUsage") + +plugins { + kotlin("multiplatform") version "2.0.0" + id("com.android.application") version "8.1.1" + id("io.deepmedia.tools.knee") version "0.3.0-SNAPSHOT" +} + +configurations.configureEach { + resolutionStrategy { + cacheChangingModulesFor(0, "seconds") + } +} + +android { + namespace = "io.deepmedia.tools.knee.tests" + compileSdk = 34 + defaultConfig { + minSdk = 26 + targetSdk = 34 + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } +} + +dependencies { + androidTestImplementation("androidx.test:runner:1.5.2") + androidTestImplementation("androidx.test:rules:1.5.0") +} + +knee { + +} + +kotlin { + jvmToolchain(11) + + // frontend + androidTarget() + + // backend + applyDefaultHierarchyTemplate { + common { + group("backend") { + withAndroidNative() + } + } + } + + androidNativeArm64() + androidNativeX64() + androidNativeArm32() + androidNativeX86() +} + +/** + * This is to make included parent build work. Kotlin Compiler Plugins have a configuration + * issue: https://youtrack.jetbrains.com/issue/KT-53477/ . We workaround this in kotlin-compiler-plugin + * by using a fat JAR, but this fat JAR is exported from the "shadow" configuration, while included builds + * read from the "default" configuration and there doesn't seem to be a clean way to solve this. + */ +configurations.matching { + it.name.startsWith("kotlin") && it.name.contains("CompilerPluginClasspath") +}.all { + isTransitive = true +} \ No newline at end of file diff --git a/tests/test-imports/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/ImportFakeFlowTests.kt b/tests/test-imports/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/ImportFakeFlowTests.kt new file mode 100644 index 0000000..c303a36 --- /dev/null +++ b/tests/test-imports/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/ImportFakeFlowTests.kt @@ -0,0 +1,143 @@ +package io.deepmedia.tools.knee.tests + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.* +import org.junit.Test +import java.lang.System.identityHashCode +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.math.abs + +class ImportFakeFlowTests { + + companion object { + init { + System.loadLibrary("test_imports") + } + } + + @Test + fun testFakeFlow_nativeCollect() = runBlocking { + val realFlow = flow { + delay(50) + emit("Hello from") + delay(100) + emit("JVM!") + } + val fakeFlow = object : io.deepmedia.tools.knee.tests.FakeFlow { + override suspend fun collect(collector: io.deepmedia.tools.knee.tests.FakeFlowCollector) { + realFlow.collect { collector.emit(it) } + } + } + val result = collectFakeFlow(fakeFlow) + check(result == "Hello from JVM!") + } + + @Test + fun testFakeFlow_jvmCollect() = runBlocking { + val flow = makeFakeFlow() + var result = "" + flow.collect { + result += it + result += " " + } + result = result.trim() + check(result == "Hello from KN!") + } + + @Test + fun testFakeSharedFlow_nativeCollect() = runBlocking { + val realFlow = flow { + delay(50) + emit("Hello JVM") + }.shareIn(CoroutineScope(EmptyCoroutineContext), SharingStarted.Lazily, 1) + val fakeFlow = object : FakeSharedFlow { + override suspend fun collect(collector: FakeFlowCollector): Nothing { + realFlow.collect { collector.emit(it) } + } + } + val result = collectFakeSharedFlow(fakeFlow) + check(result == "Hello JVM") + } + + @Test + fun testFakeSharedFlow_jvmCollect() = runBlocking { + val flow = makeFakeSharedFlow() + var first: String? = null + try { + flow.collect { + first = it + throw CancellationException("FOUND!") + } + } catch (e: CancellationException) { + if (e.message != "FOUND!") throw e + } + check(first!! == "Hello") + } + + @Test + fun testFakeMutableSharedFlow() = runBlocking { + // val flow = MutableSharedFlow(replay = 2, onBufferOverflow = BufferOverflow.SUSPEND) + val flow = makeFakeMutableSharedFlow(replay = 2, onBufferOverflow = BufferOverflow.SUSPEND) + flow.emit("Str0") + flow.emit("Str1") + // Add a collector so that tryEmit can return false + // The collector immediately takes the first item then hangs + launch(Dispatchers.IO, start = CoroutineStart.UNDISPATCHED) { + flow.collect { awaitCancellation() } + } + check(flow.tryEmit("Str2")) + check(!flow.tryEmit("Invalid")) { "Flow buffer should be full" } + val list = mutableListOf() + withTimeoutOrNull(200) { + flow.collect { list.add(it) } + Unit + } + check(list.size == 2) + check(list[0] == "Str1") + check(list[1] == "Str2") + coroutineContext.cancelChildren() + } + + @Test + fun testFakeStateFlow() = runBlocking { + val flow = makeFakeMutableSharedFlow(replay = 1, onBufferOverflow = BufferOverflow.SUSPEND) + flow.emit("Value") + + val counts = mutableListOf() + val observer = launch(Dispatchers.IO) { + flow.subscriptionCount.collect { + println("[JVM] subscriptionCount CHANGED to $it") + counts.add(it) } + } + + val subscriptions = launch(Dispatchers.IO) { + fun addSubscriber(delay: Long = 0): Job = launch(start = CoroutineStart.UNDISPATCHED) { + delay(delay) + println("[JVM] ADDING SUBSCRIBER") + coroutineContext.job.invokeOnCompletion { + println("[JVM] REMOVING SUBSCRIBER") + } + flow.collect { awaitCancellation() } + } + delay(100) + val job0 = addSubscriber() + delay(100) + val job1 = addSubscriber() + delay(100) + val job2 = addSubscriber() + delay(100) + job0.cancel() + delay(100) + job1.cancel() + delay(100) + job2.cancel() + } + + subscriptions.join() + delay(50) + observer.cancel() + check(counts == listOf(0, 1, 2, 3, 2, 1, 0)) { "Counts=$counts" } + coroutineContext.cancelChildren() + } +} diff --git a/tests/test-imports/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/ImportFlowTests.kt b/tests/test-imports/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/ImportFlowTests.kt new file mode 100644 index 0000000..cf5e810 --- /dev/null +++ b/tests/test-imports/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/ImportFlowTests.kt @@ -0,0 +1,143 @@ +package io.deepmedia.tools.knee.tests + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.* +import org.junit.Test +import java.lang.System.identityHashCode +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.math.abs + +class ImportFlowTests { + + companion object { + init { + System.loadLibrary("test_imports") + } + } + + @Test + fun testFlow_nativeCollect() = runBlocking { + val flow = flow { + delay(50) + emit("Hello from") + delay(100) + emit("JVM!") + } + val result = collectFlow(flow) + check(result == "Hello from JVM!") + } + + @Test + fun testFlow_jvmCollect() = runBlocking { + val flow = makeFlow() + val result = flow.toList().joinToString(separator = " ") + check(result == "Hello from KN!") + } + + @Test + fun testSharedFlow_nativeCollect() = runBlocking { + val flow = flow { + delay(50) + emit("Hello JVM") + }.shareIn(CoroutineScope(EmptyCoroutineContext), SharingStarted.Lazily, 1) + val result = collectSharedFlow(flow) + check(result == "Hello JVM") + } + + /** + * A tricky one. Flow first will do: + * - JVM: Flow.collectWhile called + * - JVM: Flow.collect(FlowCollector) called + * - KN: Flow.collect(FlowCollector) called + * - KN: As result come, FlowCollector.emit() called + * - JVM: FlowCollector.emit() called + * due to logic in collectWhile, an AbortFlowException is thrown + * - JVM: exception is passed to KN as message+cancellation + * - KN: exception is reconstructed as CancellationException(message) + * and it is thrown by FlowCollector.emit() + * - KN: Flow.collect(FlowCollector) rethrows the exception + * - KN: Exception is transformed to some new jthrowable + * - JVM: Throws with jthrowable + * So the exception is converted twice and the original information is lost. + * TODO: we need to keep the jthrowable + * When we receive the jthrowable, create a Throwable out of it as we do + * but also store the jthrowable somewhere, for example in the cause. + * Then when the Throwable must become a jthrowable, we should check if there's + * a jthrowable already in the cause, and if there is, just throw that. + * + * TODO: care about the opposite case too (testSharedFlow_nativeCollect) + * This will be more tricky, we must use StableRef instead of global ref and do from jni + * but the principle remains the same. + * + */ + @Test + fun testSharedFlow_jvmCollect() = runBlocking { + val flow = makeSharedFlow() + val first = flow.first() + check(first == "Hello") + } + + @Test + fun testMutableSharedFlow() = runBlocking { + val flow = makeMutableSharedFlow(replay = 2, onBufferOverflow = BufferOverflow.SUSPEND) + flow.emit("Str0") + flow.emit("Str1") + // Add a collector so that tryEmit can return false + // The collector immediately takes the first item then hangs + launch(Dispatchers.IO, start = CoroutineStart.UNDISPATCHED) { + flow.collect { awaitCancellation() } + } + check(flow.tryEmit("Str2")) + check(!flow.tryEmit("Invalid")) { "Flow buffer should be full" } + val list = mutableListOf() + withTimeoutOrNull(200) { + flow.collect { list.add(it) } + Unit + } + check(list.size == 2) + check(list[0] == "Str1") + check(list[1] == "Str2") + coroutineContext.cancelChildren() + } + + @Test + fun testStateFlow() = runBlocking { + val flow = makeMutableSharedFlow(replay = 1, onBufferOverflow = BufferOverflow.SUSPEND) + flow.emit("Value") + + val counts = mutableListOf() + val observer = launch(Dispatchers.IO) { + flow.subscriptionCount.collect { counts.add(it) } + } + + val subscriptions = launch(Dispatchers.IO) { + fun addSubscriber(delay: Long = 0): Job = launch(start = CoroutineStart.UNDISPATCHED) { + delay(delay) + println("ADDING SUBSCRIBER") + coroutineContext.job.invokeOnCompletion { + println("REMOVING SUBSCRIBER") + } + flow.collect { awaitCancellation() } + } + delay(100) + val job0 = addSubscriber() + delay(100) + val job1 = addSubscriber() + delay(100) + val job2 = addSubscriber() + delay(100) + job0.cancel() + delay(100) + job1.cancel() + delay(100) + job2.cancel() + } + + subscriptions.join() + delay(50) + observer.cancel() + check(counts == listOf(0, 1, 2, 3, 2, 1, 0)) + coroutineContext.cancelChildren() + } +} diff --git a/tests/test-imports/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/ImportLambdaTests.kt b/tests/test-imports/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/ImportLambdaTests.kt new file mode 100644 index 0000000..e751e37 --- /dev/null +++ b/tests/test-imports/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/ImportLambdaTests.kt @@ -0,0 +1,121 @@ +package io.deepmedia.tools.knee.tests + +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.runBlocking +import org.junit.Test +import java.lang.System.identityHashCode +import kotlin.math.abs + +class ImportLambdaTests { + + companion object { + init { + System.loadLibrary("test_imports") + } + } + + @Test + fun testImportedInterface_simpleLambda_property() { + val lambda: () -> Unit = { } + currentSimpleLambda = lambda + check(currentSimpleLambda == lambda) + } + + @Test + fun testImportedInterface_simpleLambda_jvmInvoke() { + val native: () -> Unit = makeSimpleLambda() + native.invoke() + } + + @Test + fun testImportedInterface_simpleLambda_nativeInvoke() { + val jvm: () -> Unit = { } + invokeSimpleLambda(jvm) + } + + @Test + fun testImportedInterface_complexLambda_property() { + val lambda: (String, Long) -> String = { a, b -> a + b } + currentComplexLambda = lambda + check(currentComplexLambda == lambda) + } + + @Test + fun testImportedInterface_complexLambda_jvmInvoke() { + val native: (String, Long) -> String = makeComplexLambda() + val result = native.invoke("Hello", 10) + check(result == "Hello10") + } + + @Test + fun testImportedInterface_complexLambda_nativeInvoke() { + val lambda: (String, Long) -> String = { a, b -> a + b } + val result = invokeComplexLambda(lambda, "Hello", 20) + check(result == "Hello20") + } + + @Test + fun testImportedInterface_complexLambda2_property() { + val lambda: (Int, UInt) -> String = { a, b -> (a + b.toInt()).toString() } + currentComplexLambda2 = lambda + check(currentComplexLambda2 == lambda) + } + + @Test + fun testImportedInterface_complexLambda2_jvmInvoke() { + val native: (Int, UInt) -> String = makeComplexLambda2() + val result = native.invoke(30, 10u) + check(result == "40") + } + + @Test + fun testImportedInterface_complexLambda2_nativeInvoke() { + val lambda: (Int, UInt) -> String = { a, b -> (a + b.toInt()).toString() } + val result = invokeComplexLambda2(lambda, 25, 25u) + check(result == "50") + } + + @Test + fun testImportedInterface_simpleSuspendLambda_property() { + val lambda: suspend () -> Unit = { } + currentSimpleSuspendLambda = lambda + check(currentSimpleSuspendLambda == lambda) + } + + @Test + fun testImportedInterface_simpleSuspendLambda_jvmInvoke() = runBlocking { + val native: suspend () -> Unit = makeSimpleSuspendLambda() + native.invoke() + } + + @Test + fun testImportedInterface_simpleSuspendLambda_nativeInvoke() = runBlocking { + val jvm: suspend () -> Unit = { } + invokeSimpleSuspendLambda(jvm) + } + + + @Test + fun testImportedInterface_complexSuspendLambda_property() { + val lambda: suspend (String, Int) -> ULong = { a, b -> (a.length + b).toULong() } + currentSuspendComplexLambda = lambda + check(currentSuspendComplexLambda == lambda) + } + + @Test + fun testImportedInterface_complexSuspendLambda_jvmInvoke() = runBlocking { + val native: suspend (String, Int) -> ULong = makeSuspendComplexLambda() + val result = native.invoke("Hello", 10) + check(result == 15.toULong()) + } + + @Test + fun testImportedInterface_complexSuspendLambda_nativeInvoke() = runBlocking { + val lambda: suspend (String, Int) -> ULong = { a, b -> (a.length + b).toULong() } + val result = invokeSuspendComplexLambda(lambda, "Hi", 20) + check(result == 22.toULong()) + } +} diff --git a/tests/test-imports/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/ImportMiscTests.kt b/tests/test-imports/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/ImportMiscTests.kt new file mode 100644 index 0000000..ecf344c --- /dev/null +++ b/tests/test-imports/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/ImportMiscTests.kt @@ -0,0 +1,30 @@ +package io.deepmedia.tools.knee.tests + +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.runBlocking +import org.junit.Test + +class ImportMiscTests { + + companion object { + init { + System.loadLibrary("test_imports") + } + } + + @Test + fun testImportedEnumNullableProperty() { + check(currentDeprecationLevel == null) + currentDeprecationLevel = DeprecationLevel.HIDDEN + check(currentDeprecationLevel == DeprecationLevel.HIDDEN) + } + + @Test + fun testImportedEnumArgumentsAndReturnType() { + check(getStrongerDeprecationLevel(DeprecationLevel.WARNING) == DeprecationLevel.ERROR) + check(getStrongerDeprecationLevel(DeprecationLevel.ERROR) == DeprecationLevel.HIDDEN) + } +} diff --git a/tests/test-imports/src/androidMain/AndroidManifest.xml b/tests/test-imports/src/androidMain/AndroidManifest.xml new file mode 100644 index 0000000..a4ce17d --- /dev/null +++ b/tests/test-imports/src/androidMain/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/tests/test-imports/src/backendMain/kotlin/FakeFlowDefinitions.kt b/tests/test-imports/src/backendMain/kotlin/FakeFlowDefinitions.kt new file mode 100644 index 0000000..70cd84b --- /dev/null +++ b/tests/test-imports/src/backendMain/kotlin/FakeFlowDefinitions.kt @@ -0,0 +1,114 @@ +package io.deepmedia.tools.knee.tests + +import io.deepmedia.tools.knee.annotations.* +import io.deepmedia.tools.knee.runtime.* +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.coroutines.cancellation.CancellationException + +// Flow is a big beast to tame so we start with fake definitions. +// These tests also have another purpose which is to ensure that KneeImport can +// deal with custom interfaces (not external) and clone them correctly in codegen + +interface FakeFlow { + suspend fun collect(collector: FakeFlowCollector) +} + +fun interface FakeFlowCollector { + suspend fun emit(value: T) +} + +interface FakeSharedFlow : FakeFlow { + override suspend fun collect(collector: FakeFlowCollector): Nothing +} + +interface FakeMutableSharedFlow : FakeSharedFlow, FakeFlowCollector { + fun tryEmit(value: T): Boolean + val subscriptionCount: FakeStateFlow + fun resetReplayCache() +} + +interface FakeStateFlow : FakeSharedFlow { + val value: T +} + +interface FakeMutableStateFlow : FakeStateFlow, FakeMutableSharedFlow { + override var value: T + fun compareAndSet(expect: T, update: T): Boolean +} + +@KneeEnum typealias BufferOverflow = kotlinx.coroutines.channels.BufferOverflow +@KneeInterface typealias StringFakeFlowCollector = FakeFlowCollector +@KneeInterface typealias StringFakeFlow = FakeFlow +@KneeInterface typealias StringFakeSharedFlow = FakeSharedFlow +@KneeInterface typealias StringFakeMutableSharedFlow = FakeMutableSharedFlow +@KneeInterface typealias StringFakeStateFlow = FakeStateFlow +@KneeInterface typealias IntFakeStateFlow = FakeStateFlow +@KneeInterface typealias IntFakeStateFlowCollector = FakeFlowCollector +@KneeInterface typealias StringFakeMutableStateFlow = FakeMutableStateFlow + +private val helloFromKnFlow = flow { + delay(100) + emit("Hello") + delay(80) + emit("from KN!") +} + +@Knee fun makeFakeFlow(): FakeFlow { + return object : FakeFlow { + override suspend fun collect(collector: FakeFlowCollector) { + helloFromKnFlow.collect { collector.emit(it) } + } + } +} +@Knee fun makeFakeSharedFlow(): FakeSharedFlow { + val shared = helloFromKnFlow.shareIn(CoroutineScope(EmptyCoroutineContext), SharingStarted.Lazily, 1) + return object : FakeSharedFlow { + override suspend fun collect(collector: FakeFlowCollector): Nothing { + shared.collect { collector.emit(it) } + } + } +} + +@Knee suspend fun collectFakeFlow(flow: FakeFlow): String { + var text = "" + flow.collect { text += "$it " } + return text.trim() +} + +@Knee suspend fun collectFakeSharedFlow(flow: FakeSharedFlow): String { + var first: String? = null + try { + flow.collect { + first = it + throw CancellationException("FOUND!") + } + } catch (e: CancellationException) { + if (e.message != "FOUND!") throw e + } + return first!! +} + +@Knee fun makeFakeMutableSharedFlow(replay: Int, onBufferOverflow: BufferOverflow): FakeMutableSharedFlow = object : FakeMutableSharedFlow { + val real = MutableSharedFlow(replay = replay, onBufferOverflow = onBufferOverflow) + override fun tryEmit(value: String): Boolean = real.tryEmit(value) + override fun resetReplayCache() = real.resetReplayCache() + override suspend fun collect(collector: FakeFlowCollector): Nothing { + println("[NATIVE] collect STARTED") + real.collect { collector.emit(it) } + } + override suspend fun emit(value: String) { + real.emit(value) + } + override val subscriptionCount: FakeStateFlow = object : FakeStateFlow { + override val value: Int get() = real.subscriptionCount.value + override suspend fun collect(collector: FakeFlowCollector): Nothing { + real.subscriptionCount.collect { + println("[NATIVE] subscriptionCount CHANGED to $it") + collector.emit(it) + } + } + } +} diff --git a/tests/test-imports/src/backendMain/kotlin/FlowDefinitions.kt b/tests/test-imports/src/backendMain/kotlin/FlowDefinitions.kt new file mode 100644 index 0000000..0b34867 --- /dev/null +++ b/tests/test-imports/src/backendMain/kotlin/FlowDefinitions.kt @@ -0,0 +1,42 @@ +package io.deepmedia.tools.knee.tests + +import io.deepmedia.tools.knee.annotations.* +import io.deepmedia.tools.knee.runtime.* +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.coroutines.cancellation.CancellationException + +@KneeInterface typealias StringFlowCollector = FlowCollector +@KneeInterface typealias StringFlow = Flow +@KneeInterface typealias StringSharedFlow = SharedFlow +@KneeInterface typealias StringMutableSharedFlow = MutableSharedFlow +@KneeInterface typealias StringStateFlow = StateFlow +@KneeInterface typealias IntStateFlow = StateFlow +@KneeInterface typealias IntStateFlowCollector = FlowCollector +@KneeInterface typealias StringMutableStateFlow = MutableStateFlow + +@Knee fun makeFlow(): Flow { + return flow { + delay(100) + emit("Hello") + delay(80) + emit("from KN!") + } +} +@Knee fun makeSharedFlow(): SharedFlow { + return makeFlow().shareIn(CoroutineScope(EmptyCoroutineContext), SharingStarted.Lazily, 1) +} + +@Knee suspend fun collectFlow(flow: Flow): String { + return flow.toList().joinToString(separator = " ") +} + +@Knee suspend fun collectSharedFlow(flow: SharedFlow): String { + return flow.first() +} + +@Knee fun makeMutableSharedFlow(replay: Int, onBufferOverflow: BufferOverflow): MutableSharedFlow { + return MutableSharedFlow(replay = replay, onBufferOverflow = onBufferOverflow) +} diff --git a/tests/test-imports/src/backendMain/kotlin/Init.kt b/tests/test-imports/src/backendMain/kotlin/Init.kt new file mode 100644 index 0000000..4e5b9a7 --- /dev/null +++ b/tests/test-imports/src/backendMain/kotlin/Init.kt @@ -0,0 +1,13 @@ +package io.deepmedia.tools.knee.tests + +import io.deepmedia.tools.knee.runtime.JavaVirtualMachine +import io.deepmedia.tools.knee.runtime.useEnv +import kotlinx.cinterop.ExperimentalForeignApi +import kotlin.experimental.ExperimentalNativeApi + +@OptIn(ExperimentalForeignApi::class, ExperimentalNativeApi::class) +@CName(externName = "JNI_OnLoad") +fun onLoad(vm: JavaVirtualMachine): Int { + vm.useEnv { io.deepmedia.tools.knee.runtime.initKnee(it) } + return 0x00010006 +} diff --git a/tests/test-imports/src/backendMain/kotlin/LambdaDefinitions.kt b/tests/test-imports/src/backendMain/kotlin/LambdaDefinitions.kt new file mode 100644 index 0000000..237c39f --- /dev/null +++ b/tests/test-imports/src/backendMain/kotlin/LambdaDefinitions.kt @@ -0,0 +1,46 @@ +package io.deepmedia.tools.knee.tests + +import io.deepmedia.tools.knee.annotations.* +import io.deepmedia.tools.knee.runtime.* +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.* + + +@KneeInterface typealias SimpleLambda = () -> Unit +@KneeInterface typealias ComplexLambda = (String, Long) -> String +// The point of ComplexLambda2 is to test two identical lambdas with different generics +@KneeInterface typealias ComplexLambda2 = (Int, UInt) -> String + +@KneeInterface typealias SimpleSuspendLambda = suspend () -> Unit +@KneeInterface typealias ComplexSuspendLambda = suspend (String, Int) -> ULong + +@Knee lateinit var currentSimpleLambda: () -> Unit +@Knee fun makeSimpleLambda(): () -> Unit = { } +@Knee fun invokeSimpleLambda(lambda: () -> Unit) { lambda.invoke() } + +@Knee lateinit var currentComplexLambda: (String, Long) -> String +@Knee fun makeComplexLambda(): (String, Long) -> String = { a, b -> a + b } +@Knee fun invokeComplexLambda(lambda: (String, Long) -> String, arg0: String, arg1: Long): String { + val result = lambda.invoke(arg0, arg1) + return result +} + +@Knee lateinit var currentComplexLambda2: (Int, UInt) -> String +@Knee fun makeComplexLambda2(): (Int, UInt) -> String = { a, b -> (a + b.toInt()).toString() } +@Knee fun invokeComplexLambda2(lambda: (Int, UInt) -> String, arg0: Int, arg1: UInt): String { + return lambda.invoke(arg0, arg1) +} + +@Knee lateinit var currentSimpleSuspendLambda: suspend () -> Unit +@Knee fun makeSimpleSuspendLambda(): suspend () -> Unit = { } +@Knee suspend fun invokeSimpleSuspendLambda(lambda: suspend () -> Unit) { + kotlinx.coroutines.delay(500) + lambda.invoke() +} + +@Knee lateinit var currentSuspendComplexLambda: suspend (String, Int) -> ULong +@Knee fun makeSuspendComplexLambda(): suspend (String, Int) -> ULong = { a, b -> (a.length + b).toULong() } +@Knee suspend fun invokeSuspendComplexLambda(lambda: suspend (String, Int) -> ULong, arg0: String, arg1: Int): ULong { + kotlinx.coroutines.delay(500) + return lambda.invoke(arg0, arg1) +} diff --git a/tests/test-imports/src/backendMain/kotlin/MiscDefinitions.kt b/tests/test-imports/src/backendMain/kotlin/MiscDefinitions.kt new file mode 100644 index 0000000..b11d384 --- /dev/null +++ b/tests/test-imports/src/backendMain/kotlin/MiscDefinitions.kt @@ -0,0 +1,20 @@ +package io.deepmedia.tools.knee.tests + +import io.deepmedia.tools.knee.annotations.* +import io.deepmedia.tools.knee.runtime.* +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.* + +@KneeEnum typealias DeprecationLevel = kotlin.DeprecationLevel + +@Knee var currentDeprecationLevel: DeprecationLevel? = null + +@Knee +fun getStrongerDeprecationLevel(level: DeprecationLevel): DeprecationLevel { + return when (level) { + DeprecationLevel.WARNING -> DeprecationLevel.ERROR + DeprecationLevel.ERROR -> DeprecationLevel.HIDDEN + DeprecationLevel.HIDDEN -> error("Nothing stronger than DeprecationLevel.HIDDEN") + } +} + diff --git a/tests/test-imports/src/backendMain/kotlin/RangeDefinitions.kt b/tests/test-imports/src/backendMain/kotlin/RangeDefinitions.kt new file mode 100644 index 0000000..67cabb1 --- /dev/null +++ b/tests/test-imports/src/backendMain/kotlin/RangeDefinitions.kt @@ -0,0 +1,11 @@ +package io.deepmedia.tools.knee.tests + +import io.deepmedia.tools.knee.annotations.* +import io.deepmedia.tools.knee.runtime.* +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.* + +@KneeInterface typealias ClosedFloatRange = ClosedRange + +@Knee var currentFloatRange: ClosedRange = 0F .. 10F + diff --git a/tests/test-interfaces/.gitignore b/tests/test-interfaces/.gitignore new file mode 100644 index 0000000..f3d6549 --- /dev/null +++ b/tests/test-interfaces/.gitignore @@ -0,0 +1 @@ +/build/ \ No newline at end of file diff --git a/tests/test-interfaces/build.gradle.kts b/tests/test-interfaces/build.gradle.kts new file mode 100644 index 0000000..92f7b3d --- /dev/null +++ b/tests/test-interfaces/build.gradle.kts @@ -0,0 +1,65 @@ +@file:Suppress("UnstableApiUsage") + +plugins { + kotlin("multiplatform") version "2.0.0" + id("com.android.application") version "8.1.1" + id("io.deepmedia.tools.knee") version "0.3.0-SNAPSHOT" +} + +configurations.configureEach { + resolutionStrategy { + cacheChangingModulesFor(0, "seconds") + } +} + +android { + namespace = "io.deepmedia.tools.knee.tests" + compileSdk = 34 + defaultConfig { + minSdk = 26 + targetSdk = 34 + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } +} + +dependencies { + androidTestImplementation("androidx.test:runner:1.5.2") + androidTestImplementation("androidx.test:rules:1.5.0") +} + +knee { + +} + +kotlin { + jvmToolchain(11) + + // frontend + androidTarget() + + // backend + applyDefaultHierarchyTemplate { + common { + group("backend") { + withAndroidNative() + } + } + } + + androidNativeArm64() + androidNativeX64() + androidNativeArm32() + androidNativeX86() +} + +/** + * This is to make included parent build work. Kotlin Compiler Plugins have a configuration + * issue: https://youtrack.jetbrains.com/issue/KT-53477/ . We workaround this in kotlin-compiler-plugin + * by using a fat JAR, but this fat JAR is exported from the "shadow" configuration, while included builds + * read from the "default" configuration and there doesn't seem to be a clean way to solve this. + */ +configurations.matching { + it.name.startsWith("kotlin") && it.name.contains("CompilerPluginClasspath") +}.all { + isTransitive = true +} \ No newline at end of file diff --git a/tests/test-interfaces/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/InnerInterfaceTests.kt b/tests/test-interfaces/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/InnerInterfaceTests.kt new file mode 100644 index 0000000..642bf15 --- /dev/null +++ b/tests/test-interfaces/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/InnerInterfaceTests.kt @@ -0,0 +1,46 @@ +package io.deepmedia.tools.knee.tests + +import org.junit.Test +import java.lang.System.identityHashCode + +class InnerInterfaceTests { + + companion object { + init { + System.loadLibrary("test_interfaces") + } + } + + val jvm = object : Outer.Inner { + override var value: Int = 24 + } + + @Test + fun testProperty_retrieveNatively() { + val kn = makeInner(0) + // update on JVM, retreive natively + jvm.value = 100 + kn.value = 100 + require(jvm.value == getInnerValue(kn)) + // update on KN, retreive natively + setInnerValue(jvm, 200) + setInnerValue(kn, 200) + require(jvm.value == getInnerValue(kn)) + } + + @Test + fun testProperty_updateNatively() { + val kn = makeInner(0) + // update natively, retreive on JVM + jvm.value = 300 + setInnerValue(kn, 300) + require(jvm.value == kn.value) + require(jvm.value == 300) + + // update natively, retreive on KN + jvm.value = 400 + setInnerValue(kn, 400) + require(getInnerValue(jvm) == getInnerValue(kn)) + require(getInnerValue(kn) == 400) + } +} diff --git a/tests/test-interfaces/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/InterfaceTests.kt b/tests/test-interfaces/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/InterfaceTests.kt new file mode 100644 index 0000000..1a512fd --- /dev/null +++ b/tests/test-interfaces/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/InterfaceTests.kt @@ -0,0 +1,105 @@ +package io.deepmedia.tools.knee.tests + +import org.junit.Test +import java.lang.System.identityHashCode + +// TODO: test garbage collection +class InterfaceTests { + + companion object { + init { + System.loadLibrary("test_interfaces") + } + } + + val jvm = object : Callback { + override var counter: UInt = 0u + override val description: String get() = "[JVM::description, ${toString()}]" + override fun describe(): String = "[JVM::describe(), ${toString()}]".also { counter++ } + override fun describeNullable(actuallyDescribe: Boolean?): String? { + return if (actuallyDescribe == true) describe() else null + } + override fun hashCode(): Int = 999999 + override fun toString(): String = "Item@${identityHashCode(this)}" + } + + @Test + fun testCodecPreservesObject() { + callback = jvm + require(callback === jvm) + } + + @Test + fun testEncodedFunctions() { + jvm.counter = 3u + require(invokeCallbackDescribe(jvm) == jvm.describe()) + require(invokeCallbackDescription(jvm) == jvm.description) + require(invokeCallbackGetCounter(jvm) == jvm.counter) + } + + @Test + fun testEncodedDefaultFunctions() { + require(invokeCallbackHashCode(jvm) == jvm.hashCode()) + require(invokeCallbackToString(jvm) == jvm.toString()) + require(invokeCallbackEquals(jvm, jvm)) + } + + @Test + fun testEncodedFunctions_native() { + val kn = createCallback() + kn.counter = 5u + require(invokeCallbackDescribe(kn) == kn.describe()) + require(invokeCallbackDescription(kn) == kn.description) + require(invokeCallbackGetCounter(kn) == kn.counter) + } + + @Test + fun testEncodedDefaultFunctions_native() { + val kn = createCallback() + require(invokeCallbackHashCode(kn) == kn.hashCode()) + require(invokeCallbackToString(kn) == kn.toString()) + require(invokeCallbackEquals(kn, kn)) + } + + + @Test + fun testNullableArguments() { + check(null == invokeCallbackDescribeNullable(jvm, false)) + check(null == invokeCallbackDescribeNullable(jvm, null)) + check(jvm.describe() == invokeCallbackDescribeNullable(jvm, true)) + + val kn = createCallback() + check(null == kn.describeNullable(false)) + check(null == kn.describeNullable(null)) + check(invokeCallbackDescribe(kn) == kn.describeNullable(true)) + } + + @Test + fun testCounterUpdate() { + val kn = createCallback() + // update on JVM, retreive natively + jvm.counter = 100u + kn.counter = 100u + require(jvm.counter == invokeCallbackGetCounter(kn)) + // update on KN, retreive natively + invokeCallbackSetCounter(jvm, 200u) + invokeCallbackSetCounter(kn, 200u) + require(jvm.counter == invokeCallbackGetCounter(kn)) + } + + @Test + fun testCounterRetreival() { + val kn = createCallback() + // update natively, retreive on JVM + jvm.counter = 300u + invokeCallbackSetCounter(kn, 300u) + require(jvm.counter == kn.counter) + require(jvm.counter == 300u) + + // update natively, retreive on KN + jvm.counter = 400u + invokeCallbackSetCounter(kn, 400u) + require(invokeCallbackGetCounter(jvm) == invokeCallbackGetCounter(kn)) + require(invokeCallbackGetCounter(kn) == 400u) + } +} diff --git a/tests/test-interfaces/src/androidMain/AndroidManifest.xml b/tests/test-interfaces/src/androidMain/AndroidManifest.xml new file mode 100644 index 0000000..a4ce17d --- /dev/null +++ b/tests/test-interfaces/src/androidMain/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/tests/test-interfaces/src/backendMain/kotlin/Definintions.kt b/tests/test-interfaces/src/backendMain/kotlin/Definintions.kt new file mode 100644 index 0000000..530c05f --- /dev/null +++ b/tests/test-interfaces/src/backendMain/kotlin/Definintions.kt @@ -0,0 +1,96 @@ +package io.deepmedia.tools.knee.tests + +import io.deepmedia.tools.knee.annotations.* +import io.deepmedia.tools.knee.runtime.* +import kotlin.experimental.ExperimentalNativeApi +import kotlin.native.identityHashCode + +@KneeInterface +interface Callback { + var counter: UInt + val description: String + fun describe(): String + fun describeNullable(actuallyDescribe: Boolean?): String? +} + +@Knee +lateinit var callback: Callback + +@OptIn(ExperimentalNativeApi::class) +@Knee +fun createCallback(): Callback { + return object : Callback { + override var counter: UInt = 0u + override fun describe(): String = "[KN::describe(), ${toString()}]".also { counter++ } + override fun describeNullable(actuallyDescribe: Boolean?): String? { + return if (actuallyDescribe == true) describe() else null + } + override val description: String get() = "[KN::description, ${toString()}]" + override fun toString(): String = "Item@${this.identityHashCode()}" + override fun hashCode(): Int = 333333 + } +} + +@Knee +fun invokeCallbackDescribe(callback: Callback): String { + return callback.describe() +} + +@Knee +fun invokeCallbackDescribeNullable(callback: Callback, actuallyDescribe: Boolean?): String? { + return callback.describeNullable(actuallyDescribe) +} + +@Knee +fun invokeCallbackDescription(callback: Callback): String { + return callback.description +} + +@Knee +fun invokeCallbackHashCode(callback: Callback): Int { + return callback.hashCode() +} + +@Knee +fun invokeCallbackToString(callback: Callback): String { + return callback.toString() +} + +@Knee +fun invokeCallbackEquals(c0: Callback, c1: Callback): Boolean { + return c0 == c1 +} + +@Knee +fun invokeCallbackGetCounter(callback: Callback): UInt { + return callback.counter +} + +@Knee +fun invokeCallbackSetCounter(callback: Callback, value: UInt) { + callback.counter = value +} + +class Outer { + @KneeInterface + interface Inner { + var value: Int + } +} + +@Knee +fun setInnerValue(inner: Outer.Inner, value: Int) { + inner.value = value +} + +@Knee +fun getInnerValue(inner: Outer.Inner): Int { + return inner.value +} + +@Knee +fun makeInner(initialValue: Int): Outer.Inner { + return object : Outer.Inner { + override var value: Int = initialValue + } +} diff --git a/tests/test-interfaces/src/backendMain/kotlin/Init.kt b/tests/test-interfaces/src/backendMain/kotlin/Init.kt new file mode 100644 index 0000000..4e5b9a7 --- /dev/null +++ b/tests/test-interfaces/src/backendMain/kotlin/Init.kt @@ -0,0 +1,13 @@ +package io.deepmedia.tools.knee.tests + +import io.deepmedia.tools.knee.runtime.JavaVirtualMachine +import io.deepmedia.tools.knee.runtime.useEnv +import kotlinx.cinterop.ExperimentalForeignApi +import kotlin.experimental.ExperimentalNativeApi + +@OptIn(ExperimentalForeignApi::class, ExperimentalNativeApi::class) +@CName(externName = "JNI_OnLoad") +fun onLoad(vm: JavaVirtualMachine): Int { + vm.useEnv { io.deepmedia.tools.knee.runtime.initKnee(it) } + return 0x00010006 +} diff --git a/tests/test-misc/.gitignore b/tests/test-misc/.gitignore new file mode 100644 index 0000000..f3d6549 --- /dev/null +++ b/tests/test-misc/.gitignore @@ -0,0 +1 @@ +/build/ \ No newline at end of file diff --git a/tests/test-misc/build.gradle.kts b/tests/test-misc/build.gradle.kts new file mode 100644 index 0000000..92f7b3d --- /dev/null +++ b/tests/test-misc/build.gradle.kts @@ -0,0 +1,65 @@ +@file:Suppress("UnstableApiUsage") + +plugins { + kotlin("multiplatform") version "2.0.0" + id("com.android.application") version "8.1.1" + id("io.deepmedia.tools.knee") version "0.3.0-SNAPSHOT" +} + +configurations.configureEach { + resolutionStrategy { + cacheChangingModulesFor(0, "seconds") + } +} + +android { + namespace = "io.deepmedia.tools.knee.tests" + compileSdk = 34 + defaultConfig { + minSdk = 26 + targetSdk = 34 + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } +} + +dependencies { + androidTestImplementation("androidx.test:runner:1.5.2") + androidTestImplementation("androidx.test:rules:1.5.0") +} + +knee { + +} + +kotlin { + jvmToolchain(11) + + // frontend + androidTarget() + + // backend + applyDefaultHierarchyTemplate { + common { + group("backend") { + withAndroidNative() + } + } + } + + androidNativeArm64() + androidNativeX64() + androidNativeArm32() + androidNativeX86() +} + +/** + * This is to make included parent build work. Kotlin Compiler Plugins have a configuration + * issue: https://youtrack.jetbrains.com/issue/KT-53477/ . We workaround this in kotlin-compiler-plugin + * by using a fat JAR, but this fat JAR is exported from the "shadow" configuration, while included builds + * read from the "default" configuration and there doesn't seem to be a clean way to solve this. + */ +configurations.matching { + it.name.startsWith("kotlin") && it.name.contains("CompilerPluginClasspath") +}.all { + isTransitive = true +} \ No newline at end of file diff --git a/tests/test-misc/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/BufferTests.kt b/tests/test-misc/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/BufferTests.kt new file mode 100644 index 0000000..41b17de --- /dev/null +++ b/tests/test-misc/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/BufferTests.kt @@ -0,0 +1,17 @@ +package io.deepmedia.tools.knee.tests + +import org.junit.Test + +class BufferTests { + + companion object { + init { + System.loadLibrary("test_misc") + } + } + + @Test + fun testBuffers() { + } + +} diff --git a/tests/test-misc/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/DefaultValuesTests.kt b/tests/test-misc/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/DefaultValuesTests.kt new file mode 100644 index 0000000..56535fb --- /dev/null +++ b/tests/test-misc/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/DefaultValuesTests.kt @@ -0,0 +1,24 @@ +package io.deepmedia.tools.knee.tests + +import org.junit.Test + + +class DefaultValuesTests { + + companion object { + init { + System.loadLibrary("test_misc") + } + } + + @Test + fun testDefaultValue_function() { + nullableWithNullDefaultValue() + } + + @Test + fun testDefaultValue_class() { + ConcreteClassWithDefaultValues().withNull() + } + +} diff --git a/tests/test-misc/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/EnumTests.kt b/tests/test-misc/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/EnumTests.kt new file mode 100644 index 0000000..19d34f0 --- /dev/null +++ b/tests/test-misc/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/EnumTests.kt @@ -0,0 +1,63 @@ +package io.deepmedia.tools.knee.tests + +import org.junit.Test +import java.lang.System.identityHashCode +import kotlin.math.abs + +class EnumTests { + + companion object { + init { + System.loadLibrary("test_misc") + } + } + + @Test + fun testProperty_topLevel() { + check(currentDay == null) + currentDay = Day.Monday + check(currentDay == Day.Monday) + } + + @Test + fun testEncodeDecode_topLevel() { + check(getDayAfter(Day.Monday) == Day.Tuesday) + check(getDayAfter(Day.Sunday) == Day.Monday) + } + + @Test + fun testList_topLevel() { + val all = getAllDays() + check(all.contains(Day.Monday)) + check(all.size == 7) + } + + @Test + fun testProperty_insideClass() { + check(currentInnerDay1 == null) + currentInnerDay1 = OuterClass.InnerDay.Friday + check(currentInnerDay1 == OuterClass.InnerDay.Friday) + } + + @Test + fun testEncodeDecode_insideClass() { + check(getInnerDay1After(OuterClass.InnerDay.Saturday) == OuterClass.InnerDay.Sunday) + check(getInnerDay1After(OuterClass.InnerDay.Monday) == OuterClass.InnerDay.Tuesday) + check(getInnerDay1After(OuterClass.InnerDay.Sunday) == OuterClass.InnerDay.Monday) + } + + @Test + fun testProperty_insideInterface() { + check(currentInnerDay2 == null) + currentInnerDay2 = OuterInterface.InnerDay.Wednesday + check(currentInnerDay2 == OuterInterface.InnerDay.Wednesday) + } + + @Test + fun testEncodeDecode_insideInterface() { + check(getInnerDay2After(OuterInterface.InnerDay.Saturday) == OuterInterface.InnerDay.Sunday) + check(getInnerDay2After(OuterInterface.InnerDay.Monday) == OuterInterface.InnerDay.Tuesday) + check(getInnerDay2After(OuterInterface.InnerDay.Sunday) == OuterInterface.InnerDay.Monday) + } + +} diff --git a/tests/test-misc/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/ExceptionTests.kt b/tests/test-misc/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/ExceptionTests.kt new file mode 100644 index 0000000..e1d86c6 --- /dev/null +++ b/tests/test-misc/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/ExceptionTests.kt @@ -0,0 +1,111 @@ +package io.deepmedia.tools.knee.tests + +import org.junit.Test + +class ExceptionTests { + + companion object { + init { + System.loadLibrary("test_misc") + } + } + + @Test + fun testExceptionWithNothing() { + try { + throwWithNothing("myMessage") + error("Should not reach") + } catch (e: Throwable) { + assert(e.message!!.contains("myMessage")) + } + } + + @Test + fun testExceptionWithUnit() { + try { + throwWithUnit("myMessage") + error("Should not reach") + } catch (e: Throwable) { + assert(e.message!!.contains("myMessage")) + } + } + + @Test + fun testCustomException_fromNative() { + try { + throwCustomException(123) + error("Should not reach") + } catch (e: CustomException) { + assert(e.code == 123) + } + } + + @Test + fun testCustomException_fromJvm() { + val thrower = object : CustomExceptionThrower { + override fun throwCustomException(code: Int) { + throw CustomException("Exception thrown from JVM.", code) + } + } + + val code = throwCustomExceptionWithThrowerAndCatchCode(thrower, 999) + assert(code == 999) + } + + @Test + fun testCustomException_crossInBothDirections() { + val msg = "Exception crossing the JNI bridge in both directions." + val thrower = object : CustomExceptionThrower { + override fun throwCustomException(code: Int) { + throw CustomException(msg, code) + } + } + try { + throwCustomExceptionWithThrower(thrower, 345) + error("Should not reach here.") + } catch (e: CustomException) { + assert(e.code == 345) { "Code mismatch: ${e.code}" } + assert(e.message == msg) { "Message mismatch: ${e.message} != $msg" } + } + } + + @Test + fun testNestedCustomException_fromNative() { + try { + throwNestedCustomException(123) + error("Should not reach") + } catch (e: CustomException.Nested) { + assert(e.code == 123) + } + } + + @Test + fun testNestedCustomException_fromJvm() { + val thrower = object : CustomExceptionThrower.Nested { + override fun throwNestedCustomException(code: Int) { + throw CustomException.Nested("Exception thrown from JVM.", code) + } + } + + val code = throwNestedCustomExceptionWithThrowerAndCatchCode(thrower, 999) + assert(code == 999) + } + + + @Test + fun testNestedCustomException_crossInBothDirections() { + val msg = "Exception crossing the JNI bridge in both directions." + val thrower = object : CustomExceptionThrower.Nested { + override fun throwNestedCustomException(code: Int) { + throw CustomException.Nested(msg, code) + } + } + try { + throwNestedCustomExceptionWithThrower(thrower, 345) + error("Should not reach here.") + } catch (e: CustomException.Nested) { + assert(e.code == 345) { "Code mismatch: ${e.code}" } + assert(e.message == msg) { "Message mismatch: ${e.message} != $msg" } + } + } +} diff --git a/tests/test-misc/src/androidMain/AndroidManifest.xml b/tests/test-misc/src/androidMain/AndroidManifest.xml new file mode 100644 index 0000000..a4ce17d --- /dev/null +++ b/tests/test-misc/src/androidMain/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/tests/test-misc/src/backendMain/kotlin/BufferDefinintions.kt b/tests/test-misc/src/backendMain/kotlin/BufferDefinintions.kt new file mode 100644 index 0000000..80943cb --- /dev/null +++ b/tests/test-misc/src/backendMain/kotlin/BufferDefinintions.kt @@ -0,0 +1,21 @@ +package io.deepmedia.tools.knee.tests + +import io.deepmedia.tools.knee.annotations.* +import io.deepmedia.tools.knee.runtime.* +import io.deepmedia.tools.knee.runtime.buffer.* + + +/* @Knee +fun simpleBuffer(buffer: ByteBuffer): Unit { +} */ + +@Knee +fun simpleBufferCallback(callback: () -> ByteBuffer): Unit { +} + +/* @Knee +suspend fun suspendingBufferCallback(callback: () -> ByteBuffer): Unit { +} + */ + +@KneeInterface typealias BufferCallback = () -> ByteBuffer \ No newline at end of file diff --git a/tests/test-misc/src/backendMain/kotlin/DefaultValuesDefinitions.kt b/tests/test-misc/src/backendMain/kotlin/DefaultValuesDefinitions.kt new file mode 100644 index 0000000..b451f9c --- /dev/null +++ b/tests/test-misc/src/backendMain/kotlin/DefaultValuesDefinitions.kt @@ -0,0 +1,21 @@ +package io.deepmedia.tools.knee.tests + +import io.deepmedia.tools.knee.annotations.* +import io.deepmedia.tools.knee.runtime.* +import io.deepmedia.tools.knee.runtime.buffer.* + + +@Knee +fun nullableWithNullDefaultValue(foo: Int? = null) { +} + +interface BaseInterfaceWithDefaultValues { + fun withNull(foo: Int? = null) +} + +@KneeClass class ConcreteClassWithDefaultValues @Knee constructor() : BaseInterfaceWithDefaultValues { + @Knee + override fun withNull(foo: Int?) { + + } +} \ No newline at end of file diff --git a/tests/test-misc/src/backendMain/kotlin/EnumDefinintions.kt b/tests/test-misc/src/backendMain/kotlin/EnumDefinintions.kt new file mode 100644 index 0000000..c78f8d9 --- /dev/null +++ b/tests/test-misc/src/backendMain/kotlin/EnumDefinintions.kt @@ -0,0 +1,50 @@ +package io.deepmedia.tools.knee.tests + +import io.deepmedia.tools.knee.annotations.* + +@KneeEnum +enum class Day { + Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday +} + +class OuterClass { + @KneeEnum + enum class InnerDay { + Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday + } +} + +interface OuterInterface { + @KneeEnum + enum class InnerDay { + Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday + } +} + +@Knee var currentDay: Day? = null +@Knee var currentInnerDay1: OuterClass.InnerDay? = null +@Knee var currentInnerDay2: OuterInterface.InnerDay? = null + +@Knee +fun getDayAfter(day: Day): Day { + if (day == Day.Sunday) return Day.Monday + else return Day.entries[Day.entries.indexOf(day) + 1] +} + +@Knee +fun getAllDays(): List { + return Day.entries.toList() +} + +@Knee +fun getInnerDay1After(day: OuterClass.InnerDay): OuterClass.InnerDay { + if (day == OuterClass.InnerDay.Sunday) return OuterClass.InnerDay.Monday + else return OuterClass.InnerDay.entries[OuterClass.InnerDay.entries.indexOf(day) + 1] +} + +@Knee +fun getInnerDay2After(day: OuterInterface.InnerDay): OuterInterface.InnerDay { + if (day == OuterInterface.InnerDay.Saturday) return OuterInterface.InnerDay.Sunday + else return OuterInterface.InnerDay.entries[OuterInterface.InnerDay.entries.indexOf(day) + 1] +} + diff --git a/tests/test-misc/src/backendMain/kotlin/ExceptionDefinitions.kt b/tests/test-misc/src/backendMain/kotlin/ExceptionDefinitions.kt new file mode 100644 index 0000000..bb98e4a --- /dev/null +++ b/tests/test-misc/src/backendMain/kotlin/ExceptionDefinitions.kt @@ -0,0 +1,96 @@ +package io.deepmedia.tools.knee.tests + +import io.deepmedia.tools.knee.annotations.* +import io.deepmedia.tools.knee.runtime.* + +@Knee +fun throwWithNothing(message: String): Nothing { + error(message) +} + +@Knee +fun throwWithUnit(message: String): Unit { + error(message) +} + +@KneeClass +class CustomException @Knee constructor(message: String, @Knee val code: Int) : RuntimeException(message) { + @Knee + override val message: String? get() = super.message + + @KneeClass + class Nested @Knee constructor(message: String, @Knee val code: Int) : RuntimeException(message) { + @Knee + override val message: String? get() = super.message + } +} + +@KneeInterface +interface CustomExceptionThrower { + fun throwCustomException(code: Int) + + @KneeInterface + interface Nested { + fun throwNestedCustomException(code: Int) + } +} + +@Knee +fun throwCustomException(code: Int) { + throw(CustomException("Custom!", code)) +} + +@Knee +fun throwCustomExceptionWithThrower(thrower: CustomExceptionThrower, code: Int) { + try { + thrower.throwCustomException(code) + error("Should not reach here.") + } catch (e: Throwable) { + if (e !is CustomException) { + error("Caught other throwable: ${e.message} ${e::class}") + } + throw e + } +} + +@Knee +fun throwCustomExceptionWithThrowerAndCatchCode(thrower: CustomExceptionThrower, code: Int): Int { + try { + thrower.throwCustomException(code) + error("Should not reach here.") + } catch (e: CustomException) { + return e.code + } catch (e: Throwable) { + error("Caught other throwable: ${e.message} ${e::class}") + } +} + +@Knee +fun throwNestedCustomException(code: Int) { + throw(CustomException.Nested("Custom!", code)) +} + +@Knee +fun throwNestedCustomExceptionWithThrower(thrower: CustomExceptionThrower.Nested, code: Int) { + try { + thrower.throwNestedCustomException(code) + error("Should not reach here.") + } catch (e: Throwable) { + if (e !is CustomException.Nested) { + error("Caught other throwable: ${e.message} ${e::class}") + } + throw e + } +} + +@Knee +fun throwNestedCustomExceptionWithThrowerAndCatchCode(thrower: CustomExceptionThrower.Nested, code: Int): Int { + try { + thrower.throwNestedCustomException(code) + error("Should not reach here.") + } catch (e: CustomException.Nested) { + return e.code + } catch (e: Throwable) { + error("Caught other throwable: ${e.message} ${e::class}") + } +} \ No newline at end of file diff --git a/tests/test-misc/src/backendMain/kotlin/Init.kt b/tests/test-misc/src/backendMain/kotlin/Init.kt new file mode 100644 index 0000000..4e5b9a7 --- /dev/null +++ b/tests/test-misc/src/backendMain/kotlin/Init.kt @@ -0,0 +1,13 @@ +package io.deepmedia.tools.knee.tests + +import io.deepmedia.tools.knee.runtime.JavaVirtualMachine +import io.deepmedia.tools.knee.runtime.useEnv +import kotlinx.cinterop.ExperimentalForeignApi +import kotlin.experimental.ExperimentalNativeApi + +@OptIn(ExperimentalForeignApi::class, ExperimentalNativeApi::class) +@CName(externName = "JNI_OnLoad") +fun onLoad(vm: JavaVirtualMachine): Int { + vm.useEnv { io.deepmedia.tools.knee.runtime.initKnee(it) } + return 0x00010006 +} diff --git a/tests/test-primitives/.gitignore b/tests/test-primitives/.gitignore new file mode 100644 index 0000000..f3d6549 --- /dev/null +++ b/tests/test-primitives/.gitignore @@ -0,0 +1 @@ +/build/ \ No newline at end of file diff --git a/tests/test-primitives/build.gradle.kts b/tests/test-primitives/build.gradle.kts new file mode 100644 index 0000000..1bdd17d --- /dev/null +++ b/tests/test-primitives/build.gradle.kts @@ -0,0 +1,71 @@ +@file:Suppress("UnstableApiUsage") + +plugins { + kotlin("multiplatform") version "2.0.0" + id("com.android.application") version "8.1.1" + id("io.deepmedia.tools.knee") version "0.3.0-SNAPSHOT" +} + +configurations.configureEach { + resolutionStrategy { + cacheChangingModulesFor(0, "seconds") + } +} + +android { + namespace = "io.deepmedia.tools.knee.tests" + compileSdk = 34 + defaultConfig { + minSdk = 26 + //noinspection EditedTargetSdkVersion + targetSdk = 34 + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + /* sourceSets { + getByName("debug").jniLibs { + this.srcDir(layout.buildDirectory.get().dir("knee").dir("bin").dir("debug")) + } + } */ +} + +dependencies { + androidTestImplementation("androidx.test:runner:1.5.2") + androidTestImplementation("androidx.test:rules:1.5.0") +} + +knee { + +} + +kotlin { + jvmToolchain(11) + + // frontend + androidTarget() + + // backend + applyDefaultHierarchyTemplate { + common { + group("backend") { + withAndroidNative() + } + } + } + + androidNativeArm64() + androidNativeX64() + androidNativeArm32() + androidNativeX86() +} + +/** + * This is to make included parent build work. Kotlin Compiler Plugins have a configuration + * issue: https://youtrack.jetbrains.com/issue/KT-53477/ . We workaround this in kotlin-compiler-plugin + * by using a fat JAR, but this fat JAR is exported from the "shadow" configuration, while included builds + * read from the "default" configuration and there doesn't seem to be a clean way to solve this. + */ +configurations.matching { + it.name.startsWith("kotlin") && it.name.contains("CompilerPluginClasspath") +}.all { + isTransitive = true +} \ No newline at end of file diff --git a/tests/test-primitives/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/NullablePrimitiveTests.kt b/tests/test-primitives/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/NullablePrimitiveTests.kt new file mode 100644 index 0000000..392b668 --- /dev/null +++ b/tests/test-primitives/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/NullablePrimitiveTests.kt @@ -0,0 +1,57 @@ +package io.deepmedia.tools.knee.tests + +import org.junit.Test +import java.lang.System.identityHashCode +import kotlin.math.abs + +class NullablePrimitiveTests { + + companion object { + init { + System.loadLibrary("test_primitives") + } + } + + @Test + fun testNullableInts() { + check("null" == printNullableInt(null)) + check("7" == printNullableInt(7)) + } + + @Test + fun testNullableString() { + check("null" == printNullableString(null)) + check("hey" == printNullableString("hey")) + } + + @Test + fun testNullableBoolean() { + check("null" == printNullableBoolean(null)) + check("true" == printNullableBoolean(true)) + } + + @Test + fun testNullableByteArray() { + check("null" == printNullableByteArray(null)) + check("[0, 1, 2]" == printNullableByteArray(byteArrayOf(0, 1, 2))) + } + + @Test + fun testNullableBooleanList() { + check("null" == printNullableBooleanList(null)) + check("[false, true]" == printNullableBooleanList(listOf(false, true))) + } + + @Test + fun testNullableListSize() { + check(null == getNullableListSize(null)) + check(4 == getNullableListSize(listOf(0, 1, 2, 3))) + } + + @Test + fun testCreateNullableList() { + check(null == createNullableList(null, null, null)) + check(listOf(1) == createNullableList(null, 1, null)) + } + +} diff --git a/tests/test-primitives/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/PrimitiveCollectionsTests.kt b/tests/test-primitives/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/PrimitiveCollectionsTests.kt new file mode 100644 index 0000000..9531dd8 --- /dev/null +++ b/tests/test-primitives/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/PrimitiveCollectionsTests.kt @@ -0,0 +1,113 @@ +package io.deepmedia.tools.knee.tests + +import org.junit.Test +import java.lang.System.identityHashCode +import kotlin.math.abs + +class PrimitiveCollectionsTests { + + companion object { + init { + System.loadLibrary("test_primitives") + } + } + + @Test + fun testIntArrays() { + val a = intArrayOf(1, 2) + val b = intArrayOf(3, 4) + check((a + b).contentEquals(sumIntArrays(a, b))) + } + + @Test + fun testIntLists() { + val a = listOf(1, 2) + val b = listOf(3, 4) + check((a + b) == sumIntLists(a, b)) + } + + @Test + fun testLongArrays() { + val a = longArrayOf(1, 2) + val b = longArrayOf(3, 4) + check((a + b).contentEquals(sumLongArrays(a, b))) + } + + @Test + fun testLongLists() { + val a = listOf(1L, 2L) + val b = listOf(3L, 4L) + check((a + b) == sumLongLists(a, b)) + } + + @Test + fun testByteArrays() { + val a = byteArrayOf(1, 2) + val b = byteArrayOf(3, 4) + check((a + b).contentEquals(sumByteArrays(a, b))) + } + + @Test + fun testByteLists() { + val a = listOf(1.toByte(), 2.toByte()) + val b = listOf(3.toByte(), 4.toByte()) + check((a + b) == sumByteLists(a, b)) + } + + @Test + fun testFloatArrays() { + val a = floatArrayOf(1F, 2F) + val b = floatArrayOf(3F, 4F) + check((a + b).contentEquals(sumFloatArrays(a, b))) + } + + @Test + fun testFloatLists() { + val a = listOf(1F, 2F) + val b = listOf(3F, 4F) + check((a + b) == sumFloatLists(a, b)) + } + + @Test + fun testDoubleArrays() { + val a = doubleArrayOf(1.0, 2.0) + val b = doubleArrayOf(3.0, 4.0) + check((a + b).contentEquals(sumDoubleArrays(a, b))) + } + + @Test + fun testDoubleLists() { + val a = listOf(1.0, 2.0) + val b = listOf(3.0, 4.0) + check((a + b) == sumDoubleLists(a, b)) + } + + @Test + fun testBooleanArrays() { + val a = booleanArrayOf(false, true) + val b = booleanArrayOf(true, false) + check((a + b).contentEquals(sumBooleanArrays(a, b))) + } + + @Test + fun testBoleanLists() { + val a = listOf(false, true) + val b = listOf(true, false) + check((a + b) == sumBooleanLists(a, b)) + } + + @Test + fun testStringArrays() { + val a = arrayOf("a0", "a1") + val b = arrayOf("b0", "b1") + check((a + b).contentEquals(sumStringArrays(a, b))) + } + + @Test + fun testStringLists() { + val a = listOf("a0", "a1") + val b = listOf("b0", "b1") + check((a + b) == sumStringLists(a, b)) + } + +} diff --git a/tests/test-primitives/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/PrimitiveTests.kt b/tests/test-primitives/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/PrimitiveTests.kt new file mode 100644 index 0000000..ae57bd5 --- /dev/null +++ b/tests/test-primitives/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/PrimitiveTests.kt @@ -0,0 +1,55 @@ +package io.deepmedia.tools.knee.tests + +import org.junit.Test +import java.lang.System.identityHashCode +import kotlin.math.abs + +class PrimitiveTests { + + companion object { + init { + System.loadLibrary("test_primitives") + } + } + + @Test + fun testInts() { + check(10 == sumInts(3, 7)) + } + + @Test + fun testLongs() { + check(10000000000L == sumLongs(3000000000L, 7000000000L)) + } + + @Test + fun testBytes() { + check(10.toByte() == sumBytes(3, 7)) + } + + @Test + fun testFloats() { + check(abs(1F - sumFloats(0.3F, 0.7F)) < 0.001F) + } + + @Test + fun testDoubles() { + check(abs(1.0 - sumDoubles(0.3, 0.7)) < 0.001) + } + + @Test + fun testStrings() { + check("Hello, world!" == sumStrings("Hello, ", "world!")) + } + + @Test + fun testBooleans() { + check(andBooleans(true, true)) + check(!andBooleans(false, true)) + check(!andBooleans(false, false)) + check(orBooleans(true, false)) + check(orBooleans(true, true)) + check(!orBooleans(false, false)) + } + +} diff --git a/tests/test-primitives/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/UnsignedPrimitiveTests.kt b/tests/test-primitives/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/UnsignedPrimitiveTests.kt new file mode 100644 index 0000000..c99ac60 --- /dev/null +++ b/tests/test-primitives/src/androidInstrumentedTest/kotlin/io/deepmedia/tools/knee/tests/UnsignedPrimitiveTests.kt @@ -0,0 +1,29 @@ +package io.deepmedia.tools.knee.tests + +import org.junit.Test +import java.lang.System.identityHashCode +import kotlin.math.abs + +class UnsignedPrimitiveTests { + + companion object { + init { + System.loadLibrary("test_primitives") + } + } + + @Test + fun testUInts() { + check(10u == sumUInts(3u, 7u)) + } + + @Test + fun testULongs() { + check(10000000000UL == sumULongs(3000000000UL, 7000000000UL)) + } + + @Test + fun testUBytes() { + check(10.toUByte() == sumUBytes(3u, 7u)) + } +} diff --git a/tests/test-primitives/src/androidMain/AndroidManifest.xml b/tests/test-primitives/src/androidMain/AndroidManifest.xml new file mode 100644 index 0000000..a4ce17d --- /dev/null +++ b/tests/test-primitives/src/androidMain/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/tests/test-primitives/src/backendMain/kotlin/Definintions.kt b/tests/test-primitives/src/backendMain/kotlin/Definintions.kt new file mode 100644 index 0000000..4b8d636 --- /dev/null +++ b/tests/test-primitives/src/backendMain/kotlin/Definintions.kt @@ -0,0 +1,45 @@ +package io.deepmedia.tools.knee.tests + +import io.deepmedia.tools.knee.annotations.* +import io.deepmedia.tools.knee.runtime.* +import kotlin.native.identityHashCode + + +@Knee fun sumInts(first: Int, second: Int): Int = first + second +@Knee fun sumFloats(first: Float, second: Float): Float = first + second +@Knee fun sumDoubles(first: Double, second: Double): Double = first + second +@Knee fun sumLongs(first: Long, second: Long): Long = first + second +@Knee fun sumBytes(first: Byte, second: Byte): Byte = (first + second).toByte() +@Knee fun sumStrings(first: String, second: String): String = (first + second) + +@Knee fun sumUInts(first: UInt, second: UInt): UInt = first + second +@Knee fun sumULongs(first: ULong, second: ULong): ULong = first + second +@Knee fun sumUBytes(first: UByte, second: UByte): UByte = (first + second).toUByte() + +// Bools with default value was triggering a bug in the bast +@Knee fun andBooleans(first: Boolean = true, second: Boolean = true): Boolean = first && second +@Knee fun orBooleans(first: Boolean, second: Boolean): Boolean = first || second + +@Knee fun sumIntArrays(first: IntArray, second: IntArray): IntArray = first + second +@Knee fun sumFloatArrays(first: FloatArray, second: FloatArray): FloatArray = first + second +@Knee fun sumDoubleArrays(first: DoubleArray, second: DoubleArray): DoubleArray = first + second +@Knee fun sumLongArrays(first: LongArray, second: LongArray): LongArray = first + second +@Knee fun sumByteArrays(first: ByteArray, second: ByteArray): ByteArray = first + second +@Knee fun sumBooleanArrays(first: BooleanArray, second: BooleanArray): BooleanArray = first + second +@Knee fun sumStringArrays(first: Array, second: Array): Array = first + second + +@Knee fun sumIntLists(first: List, second: List): List = first + second +@Knee fun sumFloatLists(first: List, second: List): List = first + second +@Knee fun sumDoubleLists(first: List, second: List): List = first + second +@Knee fun sumLongLists(first: List, second: List): List = first + second +@Knee fun sumByteLists(first: List, second: List): List = first + second +@Knee fun sumBooleanLists(first: List, second: List): List = first + second +@Knee fun sumStringLists(first: List, second: List): List = first + second + +@Knee fun printNullableInt(data: Int?): String = "$data" +@Knee fun printNullableString(data: String?): String = "$data" +@Knee fun printNullableBoolean(data: Boolean?): String = "$data" +@Knee fun printNullableByteArray(array: ByteArray?): String = "${array?.toList()}" +@Knee fun printNullableBooleanList(list: List?): String = "$list" +@Knee fun getNullableListSize(list: List?): Int? = list?.size +@Knee fun createNullableList(arg0: Int?, arg1: Int?, arg2: Int?): List? = listOfNotNull(arg0, arg1, arg2).takeIf { it.isNotEmpty() } diff --git a/tests/test-primitives/src/backendMain/kotlin/Init.kt b/tests/test-primitives/src/backendMain/kotlin/Init.kt new file mode 100644 index 0000000..4480297 --- /dev/null +++ b/tests/test-primitives/src/backendMain/kotlin/Init.kt @@ -0,0 +1,14 @@ +package io.deepmedia.tools.knee.tests + +import io.deepmedia.tools.knee.runtime.JavaVirtualMachine +import io.deepmedia.tools.knee.runtime.useEnv +import kotlinx.cinterop.ExperimentalForeignApi +import kotlin.experimental.ExperimentalNativeApi + +@OptIn(ExperimentalForeignApi::class, ExperimentalNativeApi::class) +@CName(externName = "JNI_OnLoad") +fun onLoad(vm: JavaVirtualMachine): Int { + vm.useEnv { io.deepmedia.tools.knee.runtime.initKnee(it) } + return 0x00010006 +} +