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 "