├── .gitignore ├── LICENSE ├── README.md ├── build.gradle.kts ├── buildSrc ├── build.gradle.kts ├── gradle.properties └── src │ └── main │ └── kotlin │ └── java-lib-in-kotlin.kt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── loomoroutines-bypassjpms ├── build.gradle.kts └── src │ ├── main │ ├── java │ │ ├── java │ │ │ └── lang_ │ │ │ │ └── LoomContinuation.java │ │ └── module-info.java │ ├── kotlin │ │ ├── common-bypassjpms-factory.kt │ │ └── common-bypassjpms-init.kt │ └── resources │ │ └── META-INF │ │ └── services │ │ └── dev.reformator.loomoroutines.common.internal.CoroutineFactory │ └── test │ └── java │ ├── module-info.java │ └── tests │ └── BypassJpmsTest.java ├── loomoroutines-common ├── build.gradle.kts └── src │ ├── main │ ├── java │ │ ├── dev │ │ │ └── reformator │ │ │ │ └── loomoroutines │ │ │ │ └── common │ │ │ │ └── internal │ │ │ │ └── kotlinstdlibstub │ │ │ │ ├── Intrinsics.java │ │ │ │ └── Ref.java │ │ └── module-info.java │ └── kotlin │ │ ├── common-coroutines.kt │ │ ├── common-generator.kt │ │ ├── common-utils-kotlinapi.kt │ │ ├── common-utils.kt │ │ └── internal │ │ ├── common-internal-coroutines-loom.kt │ │ ├── common-internal-coroutines.kt │ │ ├── common-internal-generator.kt │ │ ├── common-internal-registry.kt │ │ ├── common-internal-utils-kotlinapi.kt │ │ ├── common-internal-utils.kt │ │ └── kotlinstdlibstub │ │ ├── common-internal-kotlinstdlibstub-bytestreams.kt │ │ ├── common-internal-kotlinstdlibstub-collections.kt │ │ ├── common-internal-kotlinstdlibstub-enums.kt │ │ ├── common-internal-kotlinstdlibstub-exeptions.kt │ │ ├── common-internal-kotlinstdlibstub-function.kt │ │ ├── common-internal-kotlinstdlibstub-strings.kt │ │ └── common-internal-kotlinstdlibstub-types.kt │ └── test │ └── java │ ├── module-info.java │ └── tests │ └── CommonTest.java ├── loomoroutines-dispatcher ├── build.gradle.kts └── src │ ├── main │ ├── java │ │ └── module-info.java │ └── kotlin │ │ ├── dispatcher-dispatcher.kt │ │ ├── dispatcher-dispatchers.kt │ │ ├── dispatcher-utils-kotlinapi.kt │ │ ├── dispatcher-utils.kt │ │ └── internal │ │ ├── disparcher-internal-context.kt │ │ ├── dispatcher-internal-context-impl.kt │ │ ├── dispatcher-internal-loop.kt │ │ └── dispatcher-internal-utils.kt │ └── test │ └── java │ ├── module-info.java │ └── tests │ └── DispatcherTest.java ├── settings.gradle.kts ├── tests-bypassjpms-nomodule ├── build.gradle.kts └── src │ └── test │ └── java │ └── BypassJpmsNomoduleTest.java ├── tests-kotlin ├── build.gradle.kts └── src │ └── test │ ├── java │ └── module-info.java │ ├── kotlin │ └── common-test-coroutines.kt │ └── resources │ └── logback.xml └── tests-nomodule ├── build.gradle.kts └── src └── test └── java ├── ExampleGenerator.java ├── ExampleSwing.java └── NomoduleTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | !**/src/main/**/build/ 5 | !**/src/test/**/build/ 6 | 7 | ### IntelliJ IDEA ### 8 | .idea/ 9 | *.iws 10 | *.iml 11 | *.ipr 12 | out/ 13 | !**/src/main/**/out/ 14 | !**/src/test/**/out/ 15 | 16 | ### Eclipse ### 17 | .apt_generated 18 | .classpath 19 | .factorypath 20 | .project 21 | .settings 22 | .springBeans 23 | .sts4-cache 24 | bin/ 25 | !**/src/main/**/bin/ 26 | !**/src/test/**/bin/ 27 | 28 | ### NetBeans ### 29 | /nbproject/private/ 30 | /nbbuild/ 31 | /dist/ 32 | /nbdist/ 33 | /.nb-gradle/ 34 | 35 | ### VS Code ### 36 | .vscode/ 37 | 38 | ### Mac OS ### 39 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2024 Denis Berestinskii 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Maven Central](https://img.shields.io/maven-central/v/dev.reformator.loomoroutines/loomoroutines-common) 2 | # Loomoroutines 3 | Library for the native Java coroutines utilizing [Project Loom](https://openjdk.org/projects/loom/). 4 | 5 | Supports JDK 19 or higher. 6 | 7 | ## Motivation 8 | Project Loom brings [Virtual Threads](https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html) which is very valuable in server-side development. 9 | But its internal implementation allows to create full-fledged [coroutines](https://en.wikipedia.org/wiki/Coroutine), which allows to write asynchronous code in the regular synchronous style, which can be useful for GUI applications. 10 | This library provides API for those native Java coroutines. 11 | 12 | ## Usage 13 | Loomoroutines consists of 3 artifacts: 14 | 15 | ### Loomoroutines Dispatcher API 16 | The dependency is `dev.reformator.loomoroutines:loomoroutines-dispatcher:1.0.0`. It implements The Dispatcher Pattern and allows to switch between different dispatchers during code executions. 17 | The API is contained in the utility class `dev.reformator.loomoroutines.dispatcher.DispatcherUtils`. Developers can implement their own dispatchers by implementing the interface `dev.reformator.loomoroutines.dispatcher.Dispatcher`. 18 | Usage example in a GUI application: 19 | ```java 20 | import dev.reformator.loomoroutines.dispatcher.SwingDispatcher; 21 | import dev.reformator.loomoroutines.dispatcher.VirtualThreadsDispatcher; 22 | 23 | import javax.imageio.ImageIO; 24 | import javax.swing.*; 25 | 26 | import java.awt.*; 27 | import java.awt.image.BufferedImage; 28 | import java.io.IOException; 29 | import java.net.URI; 30 | import java.time.Duration; 31 | import java.util.regex.Pattern; 32 | 33 | import static dev.reformator.loomoroutines.dispatcher.DispatcherUtils.*; 34 | 35 | public class ExampleSwing { 36 | private static int pickingCatCounter = 0; 37 | 38 | private static final Pattern urlPattern = Pattern.compile("\"url\":\"([^\"]+)\""); 39 | 40 | public static void main(String[] args) { 41 | var frame = new JFrame("Cats"); 42 | frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 43 | var panel = new JPanel(); 44 | panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); 45 | var button = new JButton("Pick a cat"); 46 | var imagePanel = new ImagePanel(); 47 | panel.add(button); 48 | panel.add(imagePanel); 49 | frame.add(panel); 50 | frame.setSize(1000, 500); 51 | frame.setVisible(true); 52 | 53 | button.addActionListener(e -> dispatch(SwingDispatcher.INSTANCE, () -> { 54 | pickingCatCounter++; 55 | if (pickingCatCounter % 2 == 0) { 56 | button.setText("Pick another cat"); 57 | return null; 58 | } else { 59 | button.setText("This one!"); 60 | var cachedPickingCatCounter = pickingCatCounter; 61 | 62 | try { 63 | while (true) { 64 | var bufferedImage = doIn(VirtualThreadsDispatcher.INSTANCE, ExampleSwing::loadCatImage); 65 | if (pickingCatCounter != cachedPickingCatCounter) { 66 | return null; 67 | } 68 | 69 | imagePanel.setImage(bufferedImage); 70 | delay(Duration.ofSeconds(1)); 71 | 72 | if (pickingCatCounter != cachedPickingCatCounter) { 73 | return null; 74 | } 75 | } 76 | } catch (Throwable ex) { 77 | if (pickingCatCounter == cachedPickingCatCounter) { 78 | ex.printStackTrace(); 79 | pickingCatCounter++; 80 | button.setText("Exception: " + ex.getMessage() + ". Try again?"); 81 | } 82 | return null; 83 | } 84 | } 85 | })); 86 | } 87 | 88 | private static BufferedImage loadCatImage() { 89 | String url; 90 | { 91 | String json; 92 | try (var stream = URI.create("https://api.thecatapi.com/v1/images/search").toURL().openStream()) { 93 | json = new String(stream.readAllBytes()); 94 | } catch (IOException ex) { 95 | throw new RuntimeException(ex); 96 | } 97 | var mather = urlPattern.matcher(json); 98 | if (!mather.find()) { 99 | throw new RuntimeException("cat url is not found in json '" + json + "'"); 100 | } 101 | url = mather.group(1); 102 | } 103 | try (var stream = URI.create(url).toURL().openStream()) { 104 | return ImageIO.read(stream); 105 | } catch (IOException ex) { 106 | throw new RuntimeException(ex); 107 | } 108 | } 109 | } 110 | 111 | class ImagePanel extends JPanel { 112 | private BufferedImage image = null; 113 | 114 | public void setImage(BufferedImage image) { 115 | this.image = image; 116 | repaint(); 117 | } 118 | 119 | @Override 120 | protected void paintComponent(Graphics g) { 121 | super.paintComponent(g); 122 | if (image != null) { 123 | g.drawImage(image, 0, 0, null); 124 | } 125 | } 126 | } 127 | ``` 128 | Pay attention that the button click event handler contains potentionally long-running operations(like loading image through the Internet), but it executes them in a different dispatcher so the UI thread is not blocking. 129 | 130 | ### Loomoroutines common API 131 | The dependency is `dev.reformator.loomoroutines:loomoroutines-common:1.0.0`. 132 | It contains low-level coroutines API. Most of which is located in the utility class `dev.reformator.loomoroutines.common.CoroutineUtils`. 133 | Moreover, there is an implementation of the Generator Pattern. Example: 134 | ```java 135 | import java.math.BigInteger; 136 | 137 | import static dev.reformator.loomoroutines.common.GeneratorUtils.loomStream; 138 | 139 | public class ExampleGenerator { 140 | public static void main(String[] args) { 141 | var fibinacciStream = loomStream(scope -> { 142 | var previous = BigInteger.ZERO; 143 | var current = BigInteger.ONE; 144 | while (true) { 145 | scope.emit(current); 146 | var tmp = previous.add(current); 147 | previous = current; 148 | current = tmp; 149 | } 150 | }); 151 | fibinacciStream.limit(50).forEach(System.out::println); 152 | } 153 | } 154 | ``` 155 | 156 | ### Support library for bypassing JPMS 157 | To use Loomoroutines you have to add the JVM command line argument `--add-exports java.base/jdk.internal.vm=ALL-UNNAMED` or add this artifact in the classpath or modulepath. 158 | The corresponding dependency is `dev.reformator.loomoroutines:loomoroutines-bypassjpms:1.0.0`. 159 | And in the JDK below 21 you have to add JVM argument `--enable-preview`. -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | subprojects { 2 | group = "dev.reformator.loomoroutines" 3 | version = "1.0.0" 4 | } 5 | 6 | -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 2 | 3 | plugins { 4 | `kotlin-dsl` 5 | } 6 | 7 | repositories { 8 | mavenCentral() 9 | } 10 | 11 | dependencies { 12 | implementation("org.ow2.asm:asm-util:${properties["asmVersion"]}") 13 | implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:${properties["kotlinVersion"]}") 14 | implementation("org.jetbrains.dokka:dokka-gradle-plugin:${properties["dokkaVersion"]}") 15 | } 16 | 17 | kotlin { 18 | compilerOptions { 19 | jvmTarget = JvmTarget.JVM_21 20 | } 21 | } 22 | 23 | java { 24 | sourceCompatibility = JavaVersion.VERSION_21 25 | targetCompatibility = JavaVersion.VERSION_21 26 | } 27 | 28 | gradlePlugin { 29 | plugins { 30 | create("javaLibInKotlin") { 31 | id = "dev.reformator.javalibinkotlin" 32 | implementationClass = "dev.reformator.gradle.javalibinkotlin.JavalibInKotlinPlugin" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /buildSrc/gradle.properties: -------------------------------------------------------------------------------- 1 | asmVersion=9.6 2 | kotlinVersion=1.9.21 3 | dokkaVersion=1.9.10 4 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/java-lib-in-kotlin.kt: -------------------------------------------------------------------------------- 1 | package dev.reformator.gradle.javalibinkotlin 2 | 3 | import org.gradle.api.Plugin 4 | import org.gradle.api.Project 5 | import org.gradle.api.file.Directory 6 | import org.jetbrains.kotlin.gradle.plugin.extraProperties 7 | import org.objectweb.asm.* 8 | import org.objectweb.asm.tree.* 9 | 10 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 11 | 12 | class JavalibInKotlinPlugin: Plugin { 13 | override fun apply(target: Project) { 14 | target.extraProperties.set("kotlin.stdlib.default.dependency", "false") 15 | target.plugins.apply("org.jetbrains.kotlin.jvm") 16 | target.tasks.named("compileKotlin", KotlinCompile::class.java) { 17 | doLast { 18 | destinationDirectory.get().transform() 19 | } 20 | } 21 | } 22 | } 23 | 24 | private fun Directory.transform() { 25 | val aggregatedInfo = getAggregatedInfo() 26 | visitClasses { 27 | isTransformationNeeded(aggregatedInfo) && transform() 28 | } 29 | } 30 | 31 | private data class AggregatedInfo(val classToOuterClass: Map, val classToSourceFile: Map) 32 | 33 | private fun Directory.getAggregatedInfo(): AggregatedInfo { 34 | val classToOuterClass = mutableMapOf() 35 | val classToSourceFile = mutableMapOf() 36 | visitClasses { 37 | outerClass.let { 38 | if (it != null) { 39 | classToOuterClass[name] = it 40 | } 41 | } 42 | sourceFile.let { 43 | if (it != null) { 44 | classToSourceFile[name] = it 45 | } 46 | } 47 | false 48 | } 49 | return AggregatedInfo( 50 | classToOuterClass = classToOuterClass, 51 | classToSourceFile = classToSourceFile 52 | ) 53 | } 54 | 55 | private fun ClassNode.isTransformationNeeded(aggregatedInfo: AggregatedInfo): Boolean { 56 | val outerClassName = generateSequence(name) { aggregatedInfo.classToOuterClass[it] }.last() 57 | return !aggregatedInfo.classToSourceFile[outerClassName].orEmpty().endsWith("-kotlinapi.kt") 58 | } 59 | 60 | private fun Directory.visitClasses(doRewrite: ClassNode.() -> Boolean) { 61 | asFileTree.visit { 62 | if (!isDirectory && name.endsWith(".class")) { 63 | val classNode = open().use { 64 | val classReader = ClassReader(it) 65 | val classNode = ClassNode(Opcodes.ASM9) 66 | classReader.accept(classNode, 0) 67 | classNode 68 | } 69 | if (doRewrite(classNode)) { 70 | val classWriter = ClassWriter(0) 71 | classNode.accept(classWriter) 72 | file.writeBytes(classWriter.toByteArray()) 73 | } 74 | } 75 | } 76 | } 77 | 78 | private val typeReplacement = mapOf( 79 | "kotlin/jvm/internal/Intrinsics" to "dev/reformator/loomoroutines/common/internal/kotlinstdlibstub/Intrinsics", 80 | "kotlin/collections/CollectionsKt" to "dev/reformator/loomoroutines/common/internal/kotlinstdlibstub/CollectionsKt", 81 | "kotlin/text/StringsKt" to "dev/reformator/loomoroutines/common/internal/kotlinstdlibstub/StringsKt", 82 | "kotlin/enums/EnumEntriesKt" to "dev/reformator/loomoroutines/common/internal/kotlinstdlibstub/EnumEntriesKt", 83 | "kotlin/enums/EnumEntries" to "dev/reformator/loomoroutines/common/internal/kotlinstdlibstub/EnumEntries", 84 | "kotlin/io/ByteStreamsKt" to "dev/reformator/loomoroutines/common/internal/kotlinstdlibstub/ByteStreamsKt", 85 | 86 | "kotlin/jvm/internal/Ref" to "dev/reformator/loomoroutines/common/internal/kotlinstdlibstub/Ref", 87 | "kotlin/jvm/internal/Ref\$ObjectRef" to "dev/reformator/loomoroutines/common/internal/kotlinstdlibstub/Ref\$ObjectRef", 88 | "kotlin/jvm/internal/Ref\$IntRef" to "dev/reformator/loomoroutines/common/internal/kotlinstdlibstub/Ref\$IntRef", 89 | "kotlin/jvm/internal/Ref\$BooleanRef" to "dev/reformator/loomoroutines/common/internal/kotlinstdlibstub/Ref\$BooleanRef", 90 | 91 | "kotlin/NoWhenBranchMatchedException" to "dev/reformator/loomoroutines/common/internal/kotlinstdlibstub/KotlinException", 92 | "kotlin/KotlinNothingValueException" to "dev/reformator/loomoroutines/common/internal/kotlinstdlibstub/KotlinException", 93 | 94 | "kotlin/Unit" to "dev/reformator/loomoroutines/common/internal/kotlinstdlibstub/Unit", 95 | "kotlin/Function" to "dev/reformator/loomoroutines/common/internal/kotlinstdlibstub/Function", 96 | "kotlin/jvm/internal/FunctionBase" to "dev/reformator/loomoroutines/common/internal/kotlinstdlibstub/FunctionBase", 97 | "kotlin/jvm/internal/Lambda" to "dev/reformator/loomoroutines/common/internal/kotlinstdlibstub/Lambda", 98 | "kotlin/jvm/functions/Function0" to "dev/reformator/loomoroutines/common/internal/kotlinstdlibstub/Function0", 99 | "kotlin/jvm/functions/Function1" to "dev/reformator/loomoroutines/common/internal/kotlinstdlibstub/Function1", 100 | "kotlin/jvm/functions/Function2" to "dev/reformator/loomoroutines/common/internal/kotlinstdlibstub/Function2" 101 | ) 102 | 103 | private val interfaceImplementationsToRemove = listOf( 104 | "kotlin/jvm/internal/markers/KMappedMarker" 105 | ) 106 | 107 | private fun ClassNode.transform(): Boolean { 108 | var doTransform = false 109 | val needTranformationNotifier = { doTransform = true } 110 | transformField(superName, needTranformationNotifier) { superName = it } 111 | interfaces?.let { interfaces -> 112 | for (i in interfaces.indices) { 113 | transformField(interfaces[i], needTranformationNotifier) { interfaces[i] = it } 114 | } 115 | } 116 | fields?.forEach { field -> 117 | transformField(field.desc, needTranformationNotifier) { field.desc = it } 118 | transformField(field.signature, needTranformationNotifier) { field.signature = it } 119 | } 120 | innerClasses?.forEach { innerClass -> 121 | transformField(innerClass.name, needTranformationNotifier) { innerClass.name = it } 122 | transformField(innerClass.outerName, needTranformationNotifier) { innerClass.outerName = it } 123 | } 124 | methods?.forEach { method -> 125 | method.instructions?.let { instructions -> 126 | instructions.forEach { instruction -> 127 | if (instruction is MethodInsnNode) { 128 | transformField(instruction.owner, needTranformationNotifier) { instruction.owner = it } 129 | transformField(instruction.desc, needTranformationNotifier) { instruction.desc = it } 130 | } else if (instruction is TypeInsnNode) { 131 | transformField(instruction.desc, needTranformationNotifier) { instruction.desc = it } 132 | } else if (instruction is FieldInsnNode) { 133 | transformField(instruction.owner, needTranformationNotifier) { instruction.owner = it} 134 | transformField(instruction.desc, needTranformationNotifier) { instruction.desc = it } 135 | } else if (instruction is InvokeDynamicInsnNode) { 136 | transformField(instruction.desc, needTranformationNotifier) { instruction.desc = it } 137 | transformHandle(instruction.bsm, needTranformationNotifier) { instruction.bsm = it } 138 | for (index in instruction.bsmArgs.indices) { 139 | val arg = instruction.bsmArgs[index] 140 | if (arg is Type) { 141 | transformField(arg.descriptor, needTranformationNotifier) { instruction.bsmArgs[index] = Type.getType(it) } 142 | } else if (arg is Handle) { 143 | transformHandle(arg, needTranformationNotifier) { instruction.bsmArgs[index] = it } 144 | } 145 | } 146 | } else if (instruction is FrameNode) { 147 | transformFrame(instruction.local, needTranformationNotifier) 148 | transformFrame(instruction.stack, needTranformationNotifier) 149 | } 150 | } 151 | } 152 | method.localVariables?.forEach { variable -> 153 | transformField(variable.desc, needTranformationNotifier) { variable.desc = it } 154 | transformField(variable.signature, needTranformationNotifier) { variable.signature = it } 155 | } 156 | transformField(method.desc, needTranformationNotifier) { method.desc = it } 157 | transformField(method.signature, needTranformationNotifier) { method.signature = it } 158 | } 159 | interfaces?.removeAll(interfaceImplementationsToRemove)?.let { 160 | doTransform = doTransform || it 161 | } 162 | return doTransform 163 | } 164 | 165 | private fun transformFrame(frame: MutableList?, notifyNeedTransform: () -> Unit) { 166 | if (frame != null) { 167 | for (index in frame.indices) { 168 | val arg = frame[index] 169 | if (arg is String) { 170 | transformField(arg, notifyNeedTransform) { frame[index] = it } 171 | } 172 | } 173 | } 174 | } 175 | 176 | private fun transformHandle(handle: Handle?, notifyNeedTransform: () -> Unit, fieldSetter: (Handle) -> Unit) { 177 | if (handle != null) { 178 | var owner = handle.owner 179 | var desc = handle.desc 180 | var needTransformation = false 181 | run { 182 | @Suppress("NAME_SHADOWING") val notifyNeedTransform = { 183 | needTransformation = true 184 | } 185 | transformField(handle.owner, notifyNeedTransform) { owner = it } 186 | transformField(handle.desc, notifyNeedTransform) { desc = it } 187 | } 188 | if (needTransformation) { 189 | fieldSetter(Handle(handle.tag, owner, handle.name, desc, handle.isInterface)) 190 | notifyNeedTransform() 191 | } 192 | } 193 | } 194 | 195 | private fun transformField(field: String?, notifyNeedTransform: () -> Unit, fieldSetter: (String) -> Unit) { 196 | if (field != null) { 197 | var doTransform = false 198 | var transformed: String = field 199 | for ((originalType, replacedType) in typeReplacement) { 200 | if (transformed.contains(originalType)) { 201 | transformed = transformed.replace(originalType, replacedType) 202 | doTransform = true 203 | } 204 | } 205 | if (doTransform) { 206 | fieldSetter(transformed) 207 | notifyNeedTransform() 208 | } 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | jetbrainsAnnotationsVersion=24.1.0 2 | jupiterVersion=5.9.1 3 | kotlinxCoroutinesVersion=1.7.3 4 | log4jVersion=2.22.0 5 | slf4jVersion=2.0.9 6 | logbackVersion=1.4.14 7 | kotlinLoggingVersion=3.0.5 8 | commonsLang3Version=3.14.0 9 | jvmDriverVersion=9.6.0 10 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anamorphosee/loomoroutines/26c923ca2a3630b93c5b48bce2d7b7dcdc9f3903/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Dec 10 23:53:28 IST 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /loomoroutines-bypassjpms/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.dokka.gradle.DokkaTask 2 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 3 | import org.objectweb.asm.ClassReader 4 | import org.objectweb.asm.ClassWriter 5 | import org.objectweb.asm.Handle 6 | import org.objectweb.asm.Opcodes 7 | import org.objectweb.asm.Type 8 | import org.objectweb.asm.tree.* 9 | 10 | plugins { 11 | id("dev.reformator.javalibinkotlin") 12 | id("org.jetbrains.dokka") 13 | `maven-publish` 14 | signing 15 | } 16 | 17 | repositories { 18 | mavenCentral() 19 | } 20 | 21 | dependencies { 22 | compileOnly("org.jetbrains.kotlin:kotlin-stdlib:${kotlin.coreLibrariesVersion}") 23 | implementation(project(":loomoroutines-common")) 24 | implementation("io.github.toolfactory:jvm-driver:${properties["jvmDriverVersion"]}") 25 | implementation("org.slf4j:slf4j-api:${properties["slf4jVersion"]}") 26 | 27 | testImplementation("org.junit.jupiter:junit-jupiter-api:${properties["jupiterVersion"]}") 28 | testImplementation("org.apache.commons:commons-lang3:${properties["commonsLang3Version"]}") 29 | testRuntimeOnly("ch.qos.logback:logback-classic:${properties["logbackVersion"]}") 30 | testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${properties["jupiterVersion"]}") 31 | } 32 | 33 | java { 34 | sourceCompatibility = JavaVersion.VERSION_19 35 | targetCompatibility = JavaVersion.VERSION_19 36 | withSourcesJar() 37 | } 38 | 39 | kotlin { 40 | compilerOptions { 41 | jvmTarget = JvmTarget.JVM_19 42 | freeCompilerArgs.addAll("-Xjvm-default=all") 43 | } 44 | } 45 | 46 | tasks.withType { 47 | options.compilerArgs.addAll(listOf("--add-exports", "java.base/jdk.internal.vm=dev.reformator.loomoroutines.bypassjpms")) 48 | doLast { 49 | val continuationClassFile = destinationDirectory.get().dir("java").dir("lang_").file("LoomContinuation.class").asFile 50 | if (continuationClassFile.exists()) { 51 | val classNode = continuationClassFile.inputStream().use { 52 | val classReader = ClassReader(it) 53 | val classNode = ClassNode(Opcodes.ASM9) 54 | classReader.accept(classNode, 0) 55 | classNode 56 | } 57 | classNode.name = classNode.name.transform 58 | classNode.fields.forEach { field -> 59 | field.desc = field.desc.transform 60 | field.signature = field.signature.transform 61 | } 62 | classNode.methods.forEach { method -> 63 | method.instructions.forEach { instruction -> 64 | if (instruction is FieldInsnNode) { 65 | instruction.desc = instruction.desc.transform 66 | instruction.owner = instruction.owner.transform 67 | } else if (instruction is FrameNode) { 68 | instruction.local?.forEachIndexed { index, local -> 69 | if (local is String) { 70 | instruction.local[index] = local.transform 71 | } 72 | } 73 | instruction.stack?.forEachIndexed { index, stack -> 74 | if (stack is String) { 75 | instruction.stack[index] = stack.transform 76 | } 77 | } 78 | } else if (instruction is MethodInsnNode) { 79 | instruction.desc = instruction.desc.transform 80 | instruction.owner = instruction.owner.transform 81 | } else if (instruction is TypeInsnNode) { 82 | instruction.desc = instruction.desc.transform 83 | } else if (instruction is InvokeDynamicInsnNode) { 84 | instruction.desc = instruction.desc.transform 85 | instruction.bsm = instruction.bsm.transform 86 | for (index in instruction.bsmArgs.indices) { 87 | val arg = instruction.bsmArgs[index] 88 | if (arg is Type) { 89 | instruction.bsmArgs[index] = arg.transform 90 | } else if (arg is Handle) { 91 | instruction.bsmArgs[index] = arg.transform 92 | } 93 | } 94 | } 95 | } 96 | method.desc = method.desc.transform 97 | method.signature = method.signature.transform 98 | method.localVariables?.forEach { variable -> 99 | variable.desc = variable.desc.transform 100 | variable.signature = variable.signature.transform 101 | } 102 | } 103 | val classWriter = ClassWriter(0) 104 | classNode.accept(classWriter) 105 | destinationDirectory.get().dir("dev").dir("reformator").dir("loomoroutines") 106 | .file("LoomContinuation.class").asFile.writeBytes(classWriter.toByteArray()) 107 | continuationClassFile.delete() 108 | } 109 | } 110 | } 111 | 112 | private val String?.transform: String? 113 | get() = this?.replace("java/lang_/LoomContinuation", "java/lang/LoomContinuation") 114 | 115 | private val Handle?.transform: Handle? 116 | get() = this?.run { 117 | val owner = this.owner.transform 118 | val desc = this.desc.transform 119 | if (owner != this.owner || desc != this.desc) { 120 | Handle(tag, owner, name, desc, isInterface) 121 | } else { 122 | this 123 | } 124 | } 125 | 126 | private val Type?.transform: Type? 127 | get() = this?.run { 128 | val desc = descriptor.transform 129 | if (desc != descriptor) { 130 | Type.getType(desc) 131 | } else { 132 | this 133 | } 134 | } 135 | 136 | sourceSets { 137 | main { 138 | kotlin.destinationDirectory = java.destinationDirectory 139 | } 140 | } 141 | 142 | tasks.test { 143 | useJUnitPlatform() 144 | } 145 | 146 | val javadocJarTask = tasks.create("javadocJar", Jar::class) { 147 | dependsOn("dokkaJavadoc") 148 | archiveClassifier = "javadoc" 149 | from(tasks.named("dokkaJavadoc").get().outputDirectory) 150 | } 151 | 152 | publishing { 153 | publications { 154 | create("maven") { 155 | from(components["java"]) 156 | artifact(javadocJarTask) 157 | pom { 158 | name.set("Loomoroutines support lib. for bypassing JPMS") 159 | description.set("Library for Java native coroutines using Project Loom.") 160 | url.set("https://github.com/Anamorphosee/loomoroutines") 161 | licenses { 162 | license { 163 | name.set("The Apache License, Version 2.0") 164 | url.set("https://raw.githubusercontent.com/Anamorphosee/loomoroutines/main/LICENSE") 165 | } 166 | } 167 | developers { 168 | developer { 169 | name.set("Denis Berestinskii") 170 | email.set("berestinsky@gmail.com") 171 | url.set("https://github.com/Anamorphosee") 172 | } 173 | } 174 | scm { 175 | connection.set("scm:git:git://github.com/Anamorphosee/loomoroutines.git") 176 | developerConnection.set("scm:git:ssh://github.com:Anamorphosee/loomoroutines.git") 177 | url.set("http://github.com/Anamorphosee/loomoroutines/tree/main") 178 | } 179 | } 180 | } 181 | } 182 | repositories { 183 | maven { 184 | name = "sonatype" 185 | url = if (version.toString().endsWith("SNAPSHOT")) { 186 | uri("https://s01.oss.sonatype.org/content/repositories/snapshots/") 187 | } else { 188 | uri("https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/") 189 | } 190 | credentials { 191 | username = properties["sonatype.username"] as String? 192 | password = properties["sonatype.password"] as String? 193 | } 194 | } 195 | } 196 | } 197 | 198 | signing { 199 | useGpgCmd() 200 | sign(publishing.publications["maven"]) 201 | } 202 | -------------------------------------------------------------------------------- /loomoroutines-bypassjpms/src/main/java/java/lang_/LoomContinuation.java: -------------------------------------------------------------------------------- 1 | package java.lang_; 2 | 3 | import jdk.internal.vm.Continuation; 4 | import jdk.internal.vm.ContinuationScope; 5 | 6 | public class LoomContinuation extends Continuation { 7 | private static final ContinuationScope scope = new ContinuationScope("Loomoroutines"); 8 | private static final LoomContinuation failedLoomContinuation = 9 | new LoomContinuation<>(null, () -> { }); 10 | public static boolean assertionEnabled = true; 11 | public final T context; 12 | public LoomContinuation next = null; 13 | 14 | public LoomContinuation(T context, Runnable target) { 15 | super(scope, target); 16 | this.context = context; 17 | } 18 | 19 | public void suspend() { 20 | if (assertionEnabled) { 21 | var currentContinuation = getCurrentContinuation(); 22 | while (currentContinuation != null && currentContinuation != this) { 23 | currentContinuation = currentContinuation.next; 24 | } 25 | if (currentContinuation == null) { 26 | throw new IllegalStateException("Continuation is not in the scope."); 27 | } 28 | } 29 | var cachedNext = next; 30 | next = null; 31 | if (!Continuation.yield(scope) || next == failedLoomContinuation) { 32 | next = cachedNext; 33 | throw new IllegalStateException("Suspension has failed. Current thread is pinned."); 34 | } 35 | } 36 | 37 | public static LoomContinuation getCurrentContinuation() { 38 | return (LoomContinuation) Continuation.getCurrentContinuation(scope); 39 | } 40 | 41 | public void yieldInSuspend() { 42 | if (assertionEnabled && next == null) { 43 | throw new IllegalStateException("Assertion has failed."); 44 | } 45 | if (!Continuation.yield(scope)) { 46 | var suspendingContinuation = next; 47 | while (true) { 48 | if (suspendingContinuation.next != null) { 49 | suspendingContinuation = suspendingContinuation.next; 50 | } else { 51 | suspendingContinuation.next = failedLoomContinuation; 52 | return; 53 | } 54 | } 55 | } 56 | } 57 | 58 | @Override 59 | protected void onPinned(Pinned reason) { } 60 | } 61 | -------------------------------------------------------------------------------- /loomoroutines-bypassjpms/src/main/java/module-info.java: -------------------------------------------------------------------------------- 1 | module dev.reformator.loomoroutines.bypassjpms { 2 | requires static kotlin.stdlib; 3 | requires dev.reformator.loomoroutines.common; 4 | requires io.github.toolfactory.jvm; 5 | 6 | provides dev.reformator.loomoroutines.common.internal.CoroutineFactory with dev.reformator.loomoroutines.bypassjpms.internal.BypassJpmsCoroutineFactory; 7 | } 8 | -------------------------------------------------------------------------------- /loomoroutines-bypassjpms/src/main/kotlin/common-bypassjpms-factory.kt: -------------------------------------------------------------------------------- 1 | package dev.reformator.loomoroutines.bypassjpms.internal 2 | 3 | import dev.reformator.loomoroutines.common.NotRunningCoroutine 4 | import dev.reformator.loomoroutines.common.SuspendedCoroutine 5 | import dev.reformator.loomoroutines.common.internal.* 6 | import dev.reformator.loomoroutines.common.internal.Function 7 | import java.util.concurrent.atomic.AtomicBoolean 8 | 9 | private class LoomSuspendedCoroutine(private val continuation: Any) : SuspendedCoroutine { 10 | private val dirty = AtomicBoolean(false) 11 | 12 | @Suppress("UNCHECKED_CAST") 13 | override val coroutineContext: T 14 | get() = LoomoroutinesBypassJpmsContinuationSupport.getContext(continuation) as T 15 | 16 | override fun resume(): NotRunningCoroutine { 17 | if (dirty.compareAndSet(false, true)) { 18 | assert { LoomoroutinesBypassJpmsContinuationSupport.getNext(continuation) == null } 19 | LoomoroutinesBypassJpmsContinuationSupport.setNext( 20 | continuation, 21 | LoomoroutinesBypassJpmsContinuationSupport.getCurrentLoomContinuation() 22 | ) 23 | loop { 24 | LoomoroutinesBypassJpmsContinuationSupport.run(continuation) 25 | if (LoomoroutinesBypassJpmsContinuationSupport.isDone(continuation)) { 26 | return CompletedCoroutineImpl(coroutineContext) 27 | } 28 | if (LoomoroutinesBypassJpmsContinuationSupport.getNext(continuation) != null) { 29 | LoomoroutinesBypassJpmsContinuationSupport.yieldInSuspend(continuation) 30 | } else { 31 | return LoomSuspendedCoroutine(continuation) 32 | } 33 | } 34 | } else { 35 | error("Suspended coroutine has already resumed.") 36 | } 37 | } 38 | } 39 | 40 | class BypassJpmsCoroutineFactory: CoroutineFactory { 41 | override val isAvailable: Boolean 42 | get() = true 43 | 44 | override fun createCoroutine(context: T, body: Runnable): SuspendedCoroutine = 45 | LoomSuspendedCoroutine(LoomoroutinesBypassJpmsContinuationSupport.create(context, body)) 46 | 47 | override fun forEachRunningCoroutineContext(commandByContext: Function) { 48 | var coroutine = LoomoroutinesBypassJpmsContinuationSupport.getCurrentLoomContinuation() 49 | while (coroutine != null) { 50 | when (commandByContext(LoomoroutinesBypassJpmsContinuationSupport.getContext(coroutine))) { 51 | SuspensionCommand.BREAK -> return 52 | SuspensionCommand.CONTINUE -> coroutine = LoomoroutinesBypassJpmsContinuationSupport.getNext(coroutine) 53 | SuspensionCommand.SUSPEND_AND_BREAK -> { 54 | LoomoroutinesBypassJpmsContinuationSupport.suspend(coroutine) 55 | return 56 | } 57 | } 58 | } 59 | } 60 | 61 | override fun postInit() { 62 | LoomoroutinesBypassJpmsContinuationSupport.assertionEnabled = LoomoroutinesCommonRegistry.assertionEnabled 63 | super.postInit() 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /loomoroutines-bypassjpms/src/main/kotlin/common-bypassjpms-init.kt: -------------------------------------------------------------------------------- 1 | package dev.reformator.loomoroutines.bypassjpms.internal 2 | 3 | import io.github.toolfactory.jvm.Driver 4 | import java.lang.invoke.MethodHandles 5 | import dev.reformator.loomoroutines.common.internal.use 6 | import java.lang.invoke.MethodHandle 7 | import java.lang.invoke.MethodType 8 | import java.lang.invoke.VarHandle 9 | 10 | internal object LoomoroutinesBypassJpmsContinuationSupport { 11 | private val constructor: MethodHandle 12 | private val suspend: MethodHandle 13 | private val getCurrentContinuation: MethodHandle 14 | private val context: VarHandle 15 | private val next: VarHandle 16 | private val run: MethodHandle 17 | private val isDone: MethodHandle 18 | private val yieldInSuspend: MethodHandle 19 | private val _assertionEnabled: VarHandle 20 | 21 | init { 22 | val loomContinuationClass = loadLoomContinuationClass() 23 | val lookup = MethodHandles.lookup() 24 | constructor = lookup.findConstructor( 25 | loomContinuationClass, 26 | MethodType.methodType(Void.TYPE, Object::class.java, Runnable::class.java) 27 | ) 28 | suspend = lookup.findVirtual(loomContinuationClass, "suspend", MethodType.methodType(Void.TYPE)) 29 | getCurrentContinuation = lookup.findStatic( 30 | loomContinuationClass, 31 | "getCurrentContinuation", 32 | MethodType.methodType(loomContinuationClass) 33 | ) 34 | context = lookup.findVarHandle(loomContinuationClass, "context", Object::class.java) 35 | next = lookup.findVarHandle(loomContinuationClass, "next", loomContinuationClass) 36 | run = lookup.findVirtual(loomContinuationClass, "run", MethodType.methodType(Void.TYPE)) 37 | isDone = lookup.findVirtual(loomContinuationClass, "isDone", MethodType.methodType(Boolean::class.javaPrimitiveType)) 38 | yieldInSuspend = lookup.findVirtual(loomContinuationClass, "yieldInSuspend", MethodType.methodType(Void.TYPE)) 39 | _assertionEnabled = lookup.findStaticVarHandle(loomContinuationClass, "assertionEnabled", Boolean::class.javaPrimitiveType) 40 | } 41 | 42 | fun create(context: Any?, body: Runnable): Any = 43 | constructor.invoke(context, body) 44 | 45 | fun suspend(loomContinuation: Any) { 46 | suspend.invoke(loomContinuation) 47 | } 48 | 49 | fun getCurrentLoomContinuation(): Any? = 50 | getCurrentContinuation.invoke() 51 | 52 | fun getContext(loomContinuation: Any): Any? = 53 | context.get(loomContinuation) 54 | 55 | fun getNext(loomContinuation: Any): Any? = 56 | next.get(loomContinuation) 57 | 58 | fun setNext(loomContinuation: Any, value: Any?) { 59 | next.set(loomContinuation, value) 60 | } 61 | 62 | fun run(loomContinuation: Any) { 63 | run.invoke(loomContinuation) 64 | } 65 | 66 | fun isDone(loomContinuation: Any): Boolean = 67 | isDone.invoke(loomContinuation) as Boolean 68 | 69 | fun yieldInSuspend(loomContinuation: Any) { 70 | yieldInSuspend.invoke(loomContinuation) 71 | } 72 | 73 | var assertionEnabled: Boolean 74 | get() = _assertionEnabled.get() as Boolean 75 | set(value) { 76 | _assertionEnabled.set(value) 77 | } 78 | } 79 | 80 | private fun loadLoomContinuationClass(): Class<*> { 81 | val driver: Driver = Driver.Factory.getNew() 82 | val lookup: MethodHandles.Lookup = driver.getConsulter(Object::class.java) 83 | return lookup.defineClass(loadResource("dev/reformator/loomoroutines/LoomContinuation.class")) 84 | } 85 | 86 | @Suppress("ClassName") 87 | private class _jvmcommonStub 88 | 89 | private fun loadResource(name: String): ByteArray? { 90 | val classLoader = try { 91 | Thread.currentThread().contextClassLoader 92 | } catch (_: Throwable) { 93 | null 94 | } ?: try { 95 | _jvmcommonStub::class.java.classLoader 96 | } catch (_: Throwable) { 97 | null 98 | } ?: try { 99 | ClassLoader.getSystemClassLoader() 100 | } catch (_: Throwable) { 101 | null 102 | } 103 | val stream = if (classLoader != null) { 104 | classLoader.getResourceAsStream(name) 105 | } else { 106 | ClassLoader.getSystemResourceAsStream(name) 107 | } 108 | return stream?.use { 109 | it.readBytes() 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /loomoroutines-bypassjpms/src/main/resources/META-INF/services/dev.reformator.loomoroutines.common.internal.CoroutineFactory: -------------------------------------------------------------------------------- 1 | dev.reformator.loomoroutines.bypassjpms.internal.BypassJpmsCoroutineFactory 2 | -------------------------------------------------------------------------------- /loomoroutines-bypassjpms/src/test/java/module-info.java: -------------------------------------------------------------------------------- 1 | module tests { 2 | requires dev.reformator.loomoroutines.common; 3 | requires org.junit.jupiter.api; 4 | requires org.slf4j; 5 | requires org.apache.commons.lang3; 6 | 7 | exports tests; 8 | } 9 | -------------------------------------------------------------------------------- /loomoroutines-bypassjpms/src/test/java/tests/BypassJpmsTest.java: -------------------------------------------------------------------------------- 1 | package tests; 2 | 3 | import dev.reformator.loomoroutines.common.CompletedCoroutine; 4 | import dev.reformator.loomoroutines.common.GeneratorUtils; 5 | import dev.reformator.loomoroutines.common.SuspendedCoroutine; 6 | import org.apache.commons.lang3.mutable.MutableBoolean; 7 | import org.apache.commons.lang3.mutable.MutableInt; 8 | import org.junit.jupiter.api.Assertions; 9 | import org.junit.jupiter.api.Test; 10 | 11 | import java.util.List; 12 | import java.util.NoSuchElementException; 13 | import java.util.Objects; 14 | 15 | import static dev.reformator.loomoroutines.common.CoroutineUtils.*; 16 | import static dev.reformator.loomoroutines.common.GeneratorUtils.*; 17 | import static org.junit.jupiter.api.Assertions.*; 18 | 19 | public class BypassJpmsTest { 20 | @Test 21 | public void testGenerators() { 22 | var iterator = loomIterator(scope -> { 23 | for (int i = 0; i < 10; i++) { 24 | scope.emit(i); 25 | } 26 | scope.emit(10); 27 | for (var value: loomIterable(innerScope -> { 28 | for (int i = 11; i < 20; i++) { 29 | if (i % 2 == 0) { 30 | innerScope.emit(i); 31 | } else { 32 | scope.emit(i); 33 | } 34 | } 35 | 36 | GeneratorUtils.loomStream((innerInnerScope) -> { 37 | for (int i = 20; i <= 30; i++) { 38 | if (i % 3 == 0) { 39 | innerInnerScope.emit(i); 40 | } else if (i % 3 == 1) { 41 | innerScope.emit(i); 42 | } else { 43 | scope.emit(i); 44 | } 45 | } 46 | }).forEach(i -> { 47 | if (i % 2 == 0) { 48 | innerScope.emit(i); 49 | } else { 50 | scope.emit(i); 51 | } 52 | }); 53 | })) { 54 | scope.emit(value); 55 | } 56 | }); 57 | 58 | for (int i = 0; i<= 30; i++) { 59 | assertTrue(iterator.hasNext()); 60 | assertEquals(i, iterator.next()); 61 | } 62 | assertFalse(iterator.hasNext()); 63 | assertThrows(NoSuchElementException.class, iterator::next); 64 | } 65 | 66 | @Test 67 | public void testCoroutineContextApi() { 68 | var context1CallCounter = new MutableInt(); 69 | createCoroutine("context1", () -> { 70 | context1CallCounter.increment(); 71 | 72 | assertEquals( 73 | List.of("context1"), 74 | getRunningCoroutinesContexts() 75 | ); 76 | assertEquals(1, getRunningCoroutinesNumber()); 77 | 78 | var callCounter = new MutableInt(); 79 | assertEquals("context1", getRunningCoroutineContext(context -> { 80 | callCounter.increment(); 81 | assertEquals("context1", context); 82 | return true; 83 | })); 84 | assertEquals(1, callCounter.intValue()); 85 | 86 | var context2CallCounter = new MutableInt(); 87 | createCoroutine("context2", () -> { 88 | context2CallCounter.increment(); 89 | 90 | assertEquals( 91 | List.of("context2", "context1"), 92 | getRunningCoroutinesContexts() 93 | ); 94 | assertEquals(2, getRunningCoroutinesNumber()); 95 | 96 | callCounter.setValue(0); 97 | assertEquals("context1", getRunningCoroutineContext(context -> { 98 | boolean result; 99 | if (callCounter.intValue() == 0) { 100 | assertEquals("context2", context); 101 | result = false; 102 | } else if (callCounter.intValue() == 1) { 103 | assertEquals("context1", context); 104 | result = true; 105 | } else { 106 | throw Assertions.fail("invalid callCounter: " + callCounter.intValue()); 107 | } 108 | callCounter.increment(); 109 | return result; 110 | })); 111 | assertEquals(2, callCounter.intValue()); 112 | 113 | var context3CallCounter = new MutableInt(); 114 | var coroutine3 = createCoroutine(List.of(120), () -> { 115 | context3CallCounter.increment(); 116 | 117 | callCounter.setValue(0); 118 | assertEquals( 119 | List.of(120), 120 | getRunningCoroutineContext(List.class, list -> { 121 | callCounter.increment(); 122 | assertEquals(120, list.get(0)); 123 | return true; 124 | }) 125 | ); 126 | assertEquals(1, callCounter.intValue()); 127 | 128 | assertEquals(List.of(120), getRunningCoroutineContext(List.class)); 129 | }); 130 | assertEquals(0, context3CallCounter.intValue()); 131 | assertEquals( 132 | List.of(120), 133 | assertInstanceOf(CompletedCoroutine.class, coroutine3.resume()).getCoroutineContext() 134 | ); 135 | assertEquals(1, context3CallCounter.intValue()); 136 | }).resume(); 137 | assertEquals(1, context2CallCounter.intValue()); 138 | }).resume(); 139 | assertEquals(1, context1CallCounter.intValue()); 140 | } 141 | 142 | @Test 143 | public void testCoroutineSuspensionApi() { 144 | var callCounter = new MutableInt(); 145 | var coroutine1 = createCoroutine("context1", () -> { 146 | assertEquals(1, callCounter.getAndIncrement()); 147 | assertFalse(trySuspendCoroutine(context -> { 148 | assertEquals(2, callCounter.getAndIncrement()); 149 | assertEquals("context1", context); 150 | return false; 151 | })); 152 | assertEquals(3, callCounter.getAndIncrement()); 153 | assertTrue(trySuspendCoroutine(context -> { 154 | assertEquals(4, callCounter.getAndIncrement()); 155 | assertEquals("context1", context); 156 | return true; 157 | })); 158 | assertEquals(6, callCounter.getAndIncrement()); 159 | }); 160 | assertEquals("context1", coroutine1.getCoroutineContext()); 161 | assertEquals(0, callCounter.getAndIncrement()); 162 | var coroutine2 = assertInstanceOf(SuspendedCoroutine.class, coroutine1.resume()); 163 | assertEquals("context1", coroutine2.getCoroutineContext()); 164 | assertEquals(5, callCounter.getAndIncrement()); 165 | var coroutine3 = assertInstanceOf(CompletedCoroutine.class, coroutine2.resume()); 166 | assertEquals(7, callCounter.getAndIncrement()); 167 | assertEquals("context1", coroutine3.getCoroutineContext()); 168 | } 169 | 170 | @Test 171 | public void testPinnedVirtualThread() throws InterruptedException { 172 | var completed =new MutableBoolean(false); 173 | Thread.startVirtualThread(() -> { 174 | synchronized (new Object()) { 175 | createCoroutine("context1", () -> { 176 | try { 177 | Thread.sleep(100); 178 | } catch (InterruptedException e) { 179 | throw new RuntimeException(e); 180 | } 181 | }).resume(); 182 | } 183 | completed.setTrue(); 184 | }).join(); 185 | assertTrue(completed.getValue()); 186 | } 187 | 188 | @Test 189 | public void testFailedSuspend() { 190 | try { 191 | createCoroutine("context1", () -> { 192 | synchronized (new Object()) { 193 | createCoroutine("context2", () -> { 194 | suspendCoroutine(c -> Objects.equals(c, "context1")); 195 | }).resume(); 196 | } 197 | }).resume(); 198 | } catch (IllegalStateException e) { 199 | assertTrue(e.getMessage().contains("Current thread is pinned")); 200 | return; 201 | } 202 | fail(); 203 | } 204 | 205 | @Test 206 | public void testNotFailedSuspend() { 207 | createCoroutine("context1", () -> { 208 | synchronized (new Object()) { 209 | createCoroutine("context2", () -> { 210 | suspendCoroutine(c -> Objects.equals(c, "context2")); 211 | }).resume(); 212 | } 213 | }).resume(); 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /loomoroutines-common/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.dokka.gradle.DokkaTask 2 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 3 | 4 | plugins { 5 | id("dev.reformator.javalibinkotlin") 6 | id("org.jetbrains.dokka") 7 | `maven-publish` 8 | signing 9 | } 10 | 11 | repositories { 12 | mavenCentral() 13 | } 14 | 15 | dependencies { 16 | compileOnly("org.jetbrains.kotlin:kotlin-stdlib:${kotlin.coreLibrariesVersion}") 17 | implementation("org.slf4j:slf4j-api:${properties["slf4jVersion"]}") 18 | 19 | testImplementation("org.junit.jupiter:junit-jupiter-api:${properties["jupiterVersion"]}") 20 | testImplementation("org.apache.commons:commons-lang3:${properties["commonsLang3Version"]}") 21 | testRuntimeOnly("ch.qos.logback:logback-classic:${properties["logbackVersion"]}") 22 | testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${properties["jupiterVersion"]}") 23 | } 24 | 25 | java { 26 | sourceCompatibility = JavaVersion.VERSION_19 27 | targetCompatibility = JavaVersion.VERSION_19 28 | withSourcesJar() 29 | } 30 | 31 | kotlin { 32 | compilerOptions { 33 | jvmTarget = JvmTarget.JVM_19 34 | freeCompilerArgs.addAll("-Xjvm-default=all") 35 | } 36 | } 37 | 38 | sourceSets { 39 | main { 40 | kotlin.destinationDirectory = java.destinationDirectory 41 | } 42 | } 43 | 44 | tasks.test { 45 | useJUnitPlatform() 46 | jvmArgs("--add-exports", "java.base/jdk.internal.vm=dev.reformator.loomoroutines.common") 47 | } 48 | 49 | val javadocJarTask = tasks.create("javadocJar", Jar::class) { 50 | dependsOn("dokkaJavadoc") 51 | archiveClassifier = "javadoc" 52 | from(tasks.named("dokkaJavadoc").get().outputDirectory) 53 | } 54 | 55 | publishing { 56 | publications { 57 | create("maven") { 58 | from(components["java"]) 59 | artifact(javadocJarTask) 60 | pom { 61 | name.set("Loomoroutines common lib.") 62 | description.set("Library for Java native coroutines using Project Loom.") 63 | url.set("https://github.com/Anamorphosee/loomoroutines") 64 | licenses { 65 | license { 66 | name.set("The Apache License, Version 2.0") 67 | url.set("https://raw.githubusercontent.com/Anamorphosee/loomoroutines/main/LICENSE") 68 | } 69 | } 70 | developers { 71 | developer { 72 | name.set("Denis Berestinskii") 73 | email.set("berestinsky@gmail.com") 74 | url.set("https://github.com/Anamorphosee") 75 | } 76 | } 77 | scm { 78 | connection.set("scm:git:git://github.com/Anamorphosee/loomoroutines.git") 79 | developerConnection.set("scm:git:ssh://github.com:Anamorphosee/loomoroutines.git") 80 | url.set("http://github.com/Anamorphosee/loomoroutines/tree/main") 81 | } 82 | } 83 | } 84 | } 85 | repositories { 86 | maven { 87 | name = "sonatype" 88 | url = if (version.toString().endsWith("SNAPSHOT")) { 89 | uri("https://s01.oss.sonatype.org/content/repositories/snapshots/") 90 | } else { 91 | uri("https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/") 92 | } 93 | credentials { 94 | username = properties["sonatype.username"] as String? 95 | password = properties["sonatype.password"] as String? 96 | } 97 | } 98 | } 99 | } 100 | 101 | signing { 102 | useGpgCmd() 103 | sign(publishing.publications["maven"]) 104 | } 105 | -------------------------------------------------------------------------------- /loomoroutines-common/src/main/java/dev/reformator/loomoroutines/common/internal/kotlinstdlibstub/Intrinsics.java: -------------------------------------------------------------------------------- 1 | package dev.reformator.loomoroutines.common.internal.kotlinstdlibstub; 2 | 3 | import java.util.Objects; 4 | 5 | public class Intrinsics { 6 | private Intrinsics() { } 7 | 8 | public static void checkNotNullParameter(Object parameter, String name) { 9 | if (parameter == null) { 10 | throw new NullPointerException("parameter `" + name + "` must not be null."); 11 | } 12 | } 13 | 14 | public static void checkNotNullExpressionValue(Object parameter, String expression) { 15 | if (parameter == null) { 16 | throw new NullPointerException(expression + " must not be null."); 17 | } 18 | } 19 | 20 | public static void checkNotNull(Object object) { 21 | Objects.requireNonNull(object); 22 | } 23 | 24 | public static void checkNotNull(Object object, String message) { 25 | Objects.requireNonNull(object, message); 26 | } 27 | 28 | public static boolean areEqual(Object first, Object second) { 29 | return Objects.equals(first, second); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /loomoroutines-common/src/main/java/dev/reformator/loomoroutines/common/internal/kotlinstdlibstub/Ref.java: -------------------------------------------------------------------------------- 1 | package dev.reformator.loomoroutines.common.internal.kotlinstdlibstub; 2 | 3 | import java.io.Serializable; 4 | 5 | public class Ref { 6 | private Ref() {} 7 | 8 | public static final class ObjectRef implements Serializable { 9 | public T element; 10 | 11 | @Override 12 | public String toString() { 13 | return String.valueOf(element); 14 | } 15 | } 16 | 17 | public static final class IntRef implements Serializable { 18 | public int element; 19 | 20 | public String toString() { 21 | return String.valueOf(this.element); 22 | } 23 | } 24 | 25 | public static final class BooleanRef implements Serializable { 26 | public boolean element; 27 | 28 | public BooleanRef() { 29 | } 30 | 31 | public String toString() { 32 | return String.valueOf(this.element); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /loomoroutines-common/src/main/java/module-info.java: -------------------------------------------------------------------------------- 1 | module dev.reformator.loomoroutines.common { 2 | requires static kotlin.stdlib; 3 | requires org.slf4j; 4 | 5 | uses dev.reformator.loomoroutines.common.internal.CoroutineFactory; 6 | 7 | exports dev.reformator.loomoroutines.common; 8 | 9 | exports dev.reformator.loomoroutines.common.internal to dev.reformator.loomoroutines.dispatcher, dev.reformator.loomoroutines.bypassjpms; 10 | exports dev.reformator.loomoroutines.common.internal.kotlinstdlibstub to dev.reformator.loomoroutines.dispatcher, dev.reformator.loomoroutines.bypassjpms; 11 | } 12 | -------------------------------------------------------------------------------- /loomoroutines-common/src/main/kotlin/common-coroutines.kt: -------------------------------------------------------------------------------- 1 | package dev.reformator.loomoroutines.common 2 | 3 | /** 4 | * Not running coroutine, not started, suspended or completed. 5 | * Must be one of [CompletedCoroutine] or [SuspendedCoroutine]. 6 | * @param T the coroutine's context type 7 | */ 8 | sealed interface NotRunningCoroutine { 9 | /** 10 | * Coroutine context. 11 | */ 12 | val coroutineContext: T 13 | } 14 | 15 | /** 16 | * Completed coroutine. 17 | * @param T the coroutine's context type 18 | */ 19 | interface CompletedCoroutine: NotRunningCoroutine 20 | 21 | /** 22 | * Suspended coroutine. 23 | * @param T the coroutine's context type 24 | */ 25 | interface SuspendedCoroutine: NotRunningCoroutine { 26 | /** 27 | * Continue the coroutine in current thread. 28 | * The coroutine start or continue its execution in the method and returns from it when the coroutine suspends or completes. 29 | * @return the new coroutine state after resuming execution. 30 | */ 31 | fun resume(): NotRunningCoroutine 32 | } 33 | -------------------------------------------------------------------------------- /loomoroutines-common/src/main/kotlin/common-generator.kt: -------------------------------------------------------------------------------- 1 | @file:JvmName("GeneratorUtils") 2 | 3 | package dev.reformator.loomoroutines.common 4 | 5 | import dev.reformator.loomoroutines.common.internal.Consumer 6 | import dev.reformator.loomoroutines.common.internal.GeneratorIterator 7 | import java.util.* 8 | import java.util.stream.Stream 9 | import java.util.stream.StreamSupport 10 | 11 | /** 12 | * Generator's scope. Used for emitting elements from a generator. 13 | * @param T the generator's items type 14 | */ 15 | interface GeneratorScope { 16 | /** 17 | * Emit a [value] from the generator. 18 | */ 19 | fun emit(value: T) 20 | } 21 | 22 | /** 23 | * Create an [Iterator] that iterates over the items emitted from the [generator]. 24 | * @param generator function, that receives [GeneratorScope] and emits the elements to it 25 | */ 26 | fun loomIterator(generator: Consumer>): Iterator = 27 | GeneratorIterator(generator) 28 | 29 | /** 30 | * Create an [Iterable] that iterates over the items emitted from the [generator]. 31 | * @param generator function, that receives [GeneratorScope] and emits the elements to it 32 | */ 33 | fun loomIterable(generator: Consumer>): Iterable = 34 | Iterable { loomIterator(generator) } 35 | 36 | /** 37 | * Create a [Stream] that iterates over the items emitted from the [generator]. 38 | * @param generator function, that receives [GeneratorScope] and emits the elements to it 39 | */ 40 | fun loomStream(generator: Consumer>): Stream = 41 | StreamSupport.stream(Spliterators.spliteratorUnknownSize(loomIterator(generator), Spliterator.ORDERED), false) 42 | -------------------------------------------------------------------------------- /loomoroutines-common/src/main/kotlin/common-utils-kotlinapi.kt: -------------------------------------------------------------------------------- 1 | @file:JvmName("KotlinCoroutineUtils") 2 | 3 | package dev.reformator.loomoroutines.common 4 | 5 | import dev.reformator.loomoroutines.common.internal.Consumer 6 | import dev.reformator.loomoroutines.common.internal.Predicate 7 | import java.util.stream.Stream 8 | 9 | inline fun createCoroutine(context: T, crossinline body: () -> Unit): SuspendedCoroutine = 10 | createCoroutine(context, Runnable { body() }) 11 | 12 | inline fun getRunningCoroutineContext(crossinline predicate: (T) -> Boolean): T? = 13 | getRunningCoroutineContext(Predicate { it is T && predicate(it) }) as T? 14 | 15 | inline fun getRunningCoroutineContext(): T? = 16 | getRunningCoroutineContext { _: T -> true } 17 | 18 | inline fun trySuspendCoroutine(crossinline needSuspensionByContext: (T) -> Boolean): Boolean = 19 | trySuspendCoroutine(Predicate { it is T && needSuspensionByContext(it) }) 20 | 21 | inline fun suspendCoroutine(crossinline needSuspensionByContext: (T) -> Boolean) { 22 | suspendCoroutine(Predicate { it is T && needSuspensionByContext(it) }) 23 | } 24 | 25 | inline fun loomIterator(crossinline generator: GeneratorScope.() -> Unit): Iterator = 26 | loomIterator(Consumer { it.generator() }) 27 | 28 | inline fun loomIterable(crossinline generator: GeneratorScope.() -> Unit): Iterable = 29 | loomIterable(Consumer { it.generator() }) 30 | 31 | inline fun loomStream(crossinline generator: GeneratorScope.() -> Unit): Stream = 32 | loomStream(Consumer { it.generator() }) 33 | 34 | inline fun loomSequence(crossinline generator: GeneratorScope.() -> Unit): Sequence = 35 | Sequence { loomIterator(generator) } 36 | -------------------------------------------------------------------------------- /loomoroutines-common/src/main/kotlin/common-utils.kt: -------------------------------------------------------------------------------- 1 | @file:JvmName("CoroutineUtils") 2 | 3 | package dev.reformator.loomoroutines.common 4 | 5 | import dev.reformator.loomoroutines.common.internal.* 6 | 7 | /** 8 | * Create a new not-started coroutine. 9 | * @param T coroutine's context type 10 | * @param coroutineContext coroutine's context. Used for identify the coroutine and possibly to send and receive some data between the coroutine's executions 11 | * @param coroutineBody coroutine's body. The coroutine will execute it 12 | * @return [SuspendedCoroutine] created coroutine. The coroutine is not started 13 | */ 14 | fun createCoroutine(coroutineContext: T, coroutineBody: Runnable): SuspendedCoroutine = 15 | LoomoroutinesCommonRegistry.coroutineFactory.createCoroutine(coroutineContext, coroutineBody) 16 | 17 | /** 18 | * Contexts of the all running coroutines in the current thread. 19 | * Ordered from the most nested coroutine to the outer coroutine. 20 | */ 21 | val runningCoroutinesContexts: List 22 | get() { 23 | val result = mutableListOf() 24 | forEachRunningCoroutineContext { 25 | result.add(it) 26 | } 27 | return result 28 | } 29 | 30 | /** 31 | * Number of running coroutines in the current thread. 32 | */ 33 | val runningCoroutinesNumber: Int 34 | get() { 35 | var result = 0 36 | forEachRunningCoroutineContext { result++ } 37 | return result 38 | } 39 | 40 | /** 41 | * Find a coroutine's context by the [predicate]. 42 | * [predicate] will be called for every coroutine's context from the most nested coroutine to the outer coroutine until it will return `true`. 43 | * @return the found coroutine's context or `null` if a context matching [predicate] is not found 44 | */ 45 | fun getRunningCoroutineContext(predicate: Predicate): Any? = 46 | getRunningCoroutineContextInternal { predicate(it) } 47 | 48 | /** 49 | * Find a coroutine's context by its [type][contextType] and [predicate]. 50 | * @return the found coroutine's context or `null` if a context is not found 51 | */ 52 | @Suppress("UNCHECKED_CAST") 53 | fun getRunningCoroutineContext(contextType: Class, predicate: Predicate): T? = 54 | getRunningCoroutineContextInternal { contextType.isInstance(it) && predicate(it as T) } as T? 55 | 56 | /** 57 | * Find a coroutine's context by its [type][contextType]. 58 | * @return the found coroutine's context or `null` if a context is not found 59 | */ 60 | fun getRunningCoroutineContext(contextType: Class): T? = 61 | getRunningCoroutineContext(contextType, alwaysTruePredicate) 62 | 63 | /** 64 | * Suspend a coroutine, which context matches a [predicate][needSuspensionByContext]. 65 | * @return `true` if the matching coroutine was found. `false` - otherwise (in that case a coroutine's suspension wasn't happened) 66 | */ 67 | fun trySuspendCoroutine(needSuspensionByContext: Predicate): Boolean = 68 | trySuspendCoroutineInternal { needSuspensionByContext(it) } 69 | 70 | /** 71 | * Suspend a coroutine, which context matches a [type][contextType] and a [predicate][needSuspensionByContext]. 72 | * @return `true` if the matching coroutine was found. `false` - otherwise (in that case a coroutine's suspension wasn't happened) 73 | */ 74 | @Suppress("UNCHECKED_CAST") 75 | fun trySuspendCoroutine(contextType: Class, needSuspensionByContext: Predicate): Boolean = 76 | trySuspendCoroutineInternal { contextType.isInstance(it) && needSuspensionByContext(it as T) } 77 | 78 | /** 79 | * Same as [trySuspendCoroutine] but throws an exception in case if the coroutine's is not found. 80 | */ 81 | fun suspendCoroutine(needSuspensionByContext: Predicate) { 82 | suspendCoroutineInternal { trySuspendCoroutine(needSuspensionByContext) } 83 | } 84 | 85 | /** 86 | * Same as [trySuspendCoroutine] but throws an exception in case if the coroutine's is not found. 87 | */ 88 | fun suspendCoroutine(contextType: Class, needSuspensionByContext: Predicate) { 89 | suspendCoroutineInternal { trySuspendCoroutine(contextType, needSuspensionByContext) } 90 | } 91 | 92 | /** 93 | * @return [SuspendedCoroutine] if [this] is a not-completed coroutine, `null` otherwise 94 | */ 95 | fun NotRunningCoroutine.toSuspended(): SuspendedCoroutine? = 96 | this as? SuspendedCoroutine 97 | 98 | private inline fun getRunningCoroutineContextInternal(crossinline predicate: (Any?) -> Boolean): Any? { 99 | var result: Any? = null 100 | LoomoroutinesCommonRegistry.coroutineFactory.forEachRunningCoroutineContext { 101 | if (predicate(it)) { 102 | result = it 103 | SuspensionCommand.BREAK 104 | } else { 105 | SuspensionCommand.CONTINUE 106 | } 107 | } 108 | return result 109 | } 110 | 111 | private inline fun trySuspendCoroutineInternal(crossinline needSuspensionByContext: (Any?) -> Boolean): Boolean { 112 | var result = false 113 | LoomoroutinesCommonRegistry.coroutineFactory.forEachRunningCoroutineContext { 114 | if (needSuspensionByContext(it)) { 115 | result = true 116 | SuspensionCommand.SUSPEND_AND_BREAK 117 | } else { 118 | SuspensionCommand.CONTINUE 119 | } 120 | } 121 | return result 122 | } 123 | 124 | private inline fun suspendCoroutineInternal(trySuspend: () -> Boolean) { 125 | if (!trySuspend()) { 126 | error("Suspension had failed. Are you in the right coroutine context?") 127 | } 128 | } 129 | 130 | private inline fun forEachRunningCoroutineContext(crossinline body: (Any?) -> Unit) { 131 | LoomoroutinesCommonRegistry.coroutineFactory.forEachRunningCoroutineContext { 132 | body(it) 133 | SuspensionCommand.CONTINUE 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /loomoroutines-common/src/main/kotlin/internal/common-internal-coroutines-loom.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("JAVA_MODULE_DOES_NOT_EXPORT_PACKAGE") 2 | 3 | package dev.reformator.loomoroutines.common.internal 4 | 5 | import dev.reformator.loomoroutines.common.NotRunningCoroutine 6 | import dev.reformator.loomoroutines.common.SuspendedCoroutine 7 | import jdk.internal.vm.Continuation 8 | import jdk.internal.vm.ContinuationScope 9 | import java.util.concurrent.atomic.AtomicBoolean 10 | 11 | private val log = getLogger() 12 | 13 | private class LoomSuspendedCoroutine(private val continuation: LoomContinuation) : SuspendedCoroutine { 14 | private val dirty = AtomicBoolean(false) 15 | 16 | override val coroutineContext: T 17 | get() = continuation.coroutineContext 18 | 19 | override fun resume(): NotRunningCoroutine { 20 | if (dirty.compareAndSet(false, true)) { 21 | assert { continuation.next == null } 22 | continuation.next = getCurrentContinuation() 23 | loop { 24 | continuation.run() 25 | if (continuation.isDone) { 26 | return CompletedCoroutineImpl(coroutineContext) 27 | } 28 | if (continuation.next == null) { 29 | return LoomSuspendedCoroutine(continuation) 30 | } 31 | if (!Continuation.yield(scope)) { 32 | var suspendingContinuation = continuation.next!! 33 | while (true) { 34 | val next = suspendingContinuation.next 35 | if (next != null) { 36 | suspendingContinuation = next 37 | } else { 38 | suspendingContinuation.next = failedLoomContinuation 39 | break 40 | } 41 | } 42 | } 43 | } 44 | } else { 45 | error("Suspended coroutine has already resumed.") 46 | } 47 | } 48 | } 49 | 50 | private val scope = ContinuationScope("Loomoroutines") 51 | 52 | private val failedLoomContinuation = LoomContinuation(null) { } 53 | 54 | private class LoomContinuation( 55 | val coroutineContext: T, 56 | body: Runnable 57 | ): Continuation(scope, body) { 58 | var next: LoomContinuation<*>? = null 59 | 60 | fun suspend() { 61 | assert { 62 | var currentContinuation = getCurrentContinuation() 63 | while (currentContinuation != null && currentContinuation !== this) { 64 | currentContinuation = currentContinuation.next 65 | } 66 | currentContinuation === this 67 | } 68 | val cachedNext = next 69 | next = null 70 | if (!yield(scope) || next == failedLoomContinuation) { 71 | next = cachedNext 72 | error("Suspension has failed. Current thread is pinned.") 73 | } 74 | } 75 | 76 | override fun onPinned(reason: Pinned) { } 77 | } 78 | 79 | private fun getCurrentContinuation(): LoomContinuation<*>? = 80 | Continuation.getCurrentContinuation(scope) as LoomContinuation<*>? 81 | 82 | internal object LoomCoroutineFactory: CoroutineFactory { 83 | override val isAvailable: Boolean 84 | get() = 85 | try { 86 | LoomContinuation(null) { }.run() 87 | true 88 | } catch (e: IllegalAccessError) { 89 | false 90 | } 91 | 92 | fun getUnsupportedSuggestionMessage(): String? { 93 | try { 94 | LoomContinuation(null) { }.run() 95 | } catch (e: IllegalAccessError) { 96 | log.debug { "Continuation is not accessible." } 97 | return "Continuation is not accessible." 98 | } 99 | return null 100 | } 101 | 102 | override fun createCoroutine(context: T, body: Runnable): SuspendedCoroutine = 103 | LoomSuspendedCoroutine(LoomContinuation(context, body)) 104 | 105 | override fun forEachRunningCoroutineContext(commandByContext: Function) { 106 | var continuation = getCurrentContinuation() 107 | while (continuation != null) { 108 | when (commandByContext(continuation.coroutineContext)) { 109 | SuspensionCommand.SUSPEND_AND_BREAK -> { 110 | continuation.suspend() 111 | return 112 | } 113 | SuspensionCommand.CONTINUE -> { } 114 | SuspensionCommand.BREAK -> { 115 | return 116 | } 117 | } 118 | continuation = continuation.next 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /loomoroutines-common/src/main/kotlin/internal/common-internal-coroutines.kt: -------------------------------------------------------------------------------- 1 | package dev.reformator.loomoroutines.common.internal 2 | 3 | import dev.reformator.loomoroutines.common.CompletedCoroutine 4 | import dev.reformator.loomoroutines.common.SuspendedCoroutine 5 | 6 | enum class SuspensionCommand { 7 | CONTINUE, BREAK, SUSPEND_AND_BREAK 8 | } 9 | 10 | interface CoroutineFactory { 11 | val isAvailable: Boolean 12 | 13 | fun createCoroutine(context: T, body: Runnable): SuspendedCoroutine 14 | 15 | fun forEachRunningCoroutineContext(commandByContext: Function) 16 | 17 | fun postInit() { } 18 | } 19 | 20 | class CompletedCoroutineImpl(override val coroutineContext: T) : CompletedCoroutine 21 | -------------------------------------------------------------------------------- /loomoroutines-common/src/main/kotlin/internal/common-internal-generator.kt: -------------------------------------------------------------------------------- 1 | package dev.reformator.loomoroutines.common.internal 2 | 3 | import dev.reformator.loomoroutines.common.* 4 | 5 | internal class GeneratorIterator(generator: Consumer>): Iterator { 6 | private val state = atomic>(NotBufferedGeneratorIteratorState) 7 | private var coroutine: SuspendedCoroutine> 8 | 9 | init { 10 | val context = GeneratorIteratorContext() 11 | coroutine = createCoroutine(context) { generator(context) } 12 | } 13 | 14 | override fun hasNext(): Boolean = 15 | buffer() 16 | 17 | override fun next(): T { 18 | loop { 19 | if (!buffer()) { 20 | throw NoSuchElementException("Generator has already finished.") 21 | } 22 | val state = state.value 23 | if (state is BufferedGeneratorIteratorState 24 | && this.state.cas(state, NotBufferedGeneratorIteratorState)) { 25 | return state.buffer 26 | } 27 | } 28 | } 29 | 30 | private fun buffer(): Boolean { 31 | loop { 32 | if (state.cas(NotBufferedGeneratorIteratorState, BufferingGeneratorIteratorState)) { 33 | try { 34 | when (val nextCoroutine = coroutine.resume()) { 35 | is SuspendedCoroutine> -> { 36 | coroutine = nextCoroutine 37 | ifAssert(assertBody = { 38 | state.cas( 39 | expectedValue = BufferingGeneratorIteratorState, 40 | newValue = BufferedGeneratorIteratorState(nextCoroutine.coroutineContext.buffer) 41 | ) 42 | }, notAssertBody = { 43 | state.value = BufferedGeneratorIteratorState(nextCoroutine.coroutineContext.buffer) 44 | }) 45 | return true 46 | } 47 | is CompletedCoroutine<*> -> { 48 | ifAssert(assertBody = { 49 | state.cas( 50 | expectedValue = BufferingGeneratorIteratorState, 51 | newValue = FinishedGeneratorIteratorState 52 | ) 53 | }, notAssertBody = { 54 | state.value = FinishedGeneratorIteratorState 55 | }) 56 | return false 57 | } 58 | } 59 | } catch (e: Throwable) { 60 | ifAssert(assertBody = { 61 | state.cas(BufferingGeneratorIteratorState, ExceptionalGeneratorIteratorState(e)) 62 | }, notAssertBody = { 63 | state.value = ExceptionalGeneratorIteratorState(e) 64 | }) 65 | throw e 66 | } 67 | } 68 | when (val state = state.value) { 69 | NotBufferedGeneratorIteratorState -> return@loop 70 | BufferingGeneratorIteratorState -> error("Generator is already running in an another thread.") 71 | is BufferedGeneratorIteratorState<*> -> return true 72 | is ExceptionalGeneratorIteratorState -> throw RuntimeException(state.exception) 73 | FinishedGeneratorIteratorState -> return false 74 | } 75 | } 76 | } 77 | } 78 | 79 | private sealed interface GeneratorIteratorState 80 | private data object NotBufferedGeneratorIteratorState: GeneratorIteratorState 81 | private data object BufferingGeneratorIteratorState: GeneratorIteratorState 82 | private class BufferedGeneratorIteratorState(val buffer: T): GeneratorIteratorState 83 | private class ExceptionalGeneratorIteratorState(val exception: Throwable): GeneratorIteratorState 84 | private data object FinishedGeneratorIteratorState: GeneratorIteratorState 85 | 86 | private class GeneratorIteratorContext: GeneratorScope { 87 | private var _buffer: T? = null 88 | 89 | override fun emit(value: T) { 90 | suspendCoroutine { it: Any? -> 91 | val suspend = it === this 92 | if (suspend) { 93 | _buffer = value 94 | } 95 | suspend 96 | } 97 | } 98 | 99 | val buffer: T 100 | get() { 101 | @Suppress("UNCHECKED_CAST") 102 | val result = _buffer as T 103 | _buffer = null 104 | return result 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /loomoroutines-common/src/main/kotlin/internal/common-internal-registry.kt: -------------------------------------------------------------------------------- 1 | package dev.reformator.loomoroutines.common.internal 2 | 3 | import java.util.* 4 | 5 | object LoomoroutinesCommonRegistry { 6 | internal val coroutineFactory: CoroutineFactory = 7 | if (LoomCoroutineFactory.isAvailable) { 8 | LoomCoroutineFactory 9 | } else run { 10 | ServiceLoader.load(CoroutineFactory::class.java).forEach { it: CoroutineFactory -> 11 | if (it.isAvailable) { 12 | return@run it 13 | } 14 | } 15 | val moduleName = LoomCoroutineFactory::class.java.module.let { it: Module -> 16 | if (it.isNamed) { 17 | it.name 18 | } else { 19 | "ALL-UNNAMED" 20 | } 21 | } 22 | error("Loomoroutines is not available. Please add '--add-exports java.base/jdk.internal.vm=$moduleName' " + 23 | "JVM start arguments or add the dependency 'dev.reformator.loomoroutines:loomoroutines-bypassjpms'.") 24 | } 25 | 26 | val assertionEnabled = 27 | System.getProperty("dev.reformator.loomoroutines.common.assertionEnabled")?.toBoolean() 28 | ?: javaClass.desiredAssertionStatus() 29 | 30 | init { 31 | coroutineFactory.postInit() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /loomoroutines-common/src/main/kotlin/internal/common-internal-utils-kotlinapi.kt: -------------------------------------------------------------------------------- 1 | package dev.reformator.loomoroutines.common.internal 2 | 3 | import org.slf4j.Logger 4 | 5 | inline fun List.forEachReversed(action: (T) -> Unit) { 6 | val iterator = this.listIterator(size) 7 | while (iterator.hasPrevious()) { 8 | val element = iterator.previous() 9 | action(element) 10 | } 11 | } 12 | 13 | inline fun Logger.info(e: Throwable, message: () -> String) { 14 | if (isInfoEnabled) { 15 | info(message(), e) 16 | } 17 | } 18 | 19 | inline fun Logger.info(message: () -> String) { 20 | if (isInfoEnabled) { 21 | info(message()) 22 | } 23 | } 24 | 25 | inline fun Logger.debug(e: Throwable, message: () -> String) { 26 | if (isDebugEnabled) { 27 | debug(message(), e) 28 | } 29 | } 30 | 31 | inline fun Logger.debug(message: () -> String) { 32 | if (isDebugEnabled) { 33 | debug(message()) 34 | } 35 | } 36 | 37 | inline fun Logger.trace(e: Throwable, message: () -> String) { 38 | if (isTraceEnabled) { 39 | trace(message(), e) 40 | } 41 | } 42 | 43 | inline fun Logger.trace(message: () -> String) { 44 | if (isTraceEnabled) { 45 | trace(message()) 46 | } 47 | } 48 | 49 | inline fun Logger.error(e: Throwable, message: () -> String) { 50 | if (isErrorEnabled) { 51 | error(message(), e) 52 | } 53 | } 54 | 55 | inline fun Logger.error(message: () -> String) { 56 | if (isErrorEnabled) { 57 | error(message()) 58 | } 59 | } 60 | 61 | inline fun loop(body: () -> Unit): Nothing { 62 | while (true) { 63 | body() 64 | } 65 | } 66 | 67 | inline fun assert(message: String = "Assertion check failed.", body: () -> Boolean) { 68 | if (LoomoroutinesCommonRegistry.assertionEnabled && !body()) { 69 | error(message) 70 | } 71 | } 72 | 73 | inline fun ifAssert(message: String = "Assertion check failed.", assertBody: () -> Boolean, notAssertBody: () -> Unit) { 74 | if (LoomoroutinesCommonRegistry.assertionEnabled) { 75 | if (!assertBody()) { 76 | error(message) 77 | } 78 | } else { 79 | notAssertBody() 80 | } 81 | } 82 | 83 | @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") 84 | inline fun T.use(block: (T) -> R): R { 85 | var exception: Throwable? = null 86 | return try { 87 | block(this) 88 | } catch (e: Throwable) { 89 | exception = e 90 | throw e 91 | } finally { 92 | try { 93 | close() 94 | } catch (e: Throwable) { 95 | if (exception != null) { 96 | (e as java.lang.Throwable).addSuppressed(exception) 97 | } 98 | throw e 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /loomoroutines-common/src/main/kotlin/internal/common-internal-utils.kt: -------------------------------------------------------------------------------- 1 | package dev.reformator.loomoroutines.common.internal 2 | 3 | import dev.reformator.loomoroutines.common.internal.kotlinstdlibstub.Ref 4 | import org.slf4j.Logger 5 | import org.slf4j.LoggerFactory 6 | import java.util.concurrent.atomic.AtomicReference 7 | import java.util.function.Predicate as JavaPredicate 8 | import java.util.function.Supplier as JavaSupplier 9 | import java.util.function.Consumer as JavaConsumer 10 | import java.util.function.Function as JavaFunction 11 | 12 | fun Collection.copyList(): MutableList = 13 | ArrayList(this) 14 | 15 | fun getLogger(): Logger { 16 | val trace = Exception().stackTrace 17 | val index = trace.indexOfFirst { !it.className.startsWith("java") } + 1 18 | return LoggerFactory.getLogger(trace[index].className) 19 | } 20 | 21 | typealias Supplier = JavaSupplier 22 | 23 | typealias Consumer = JavaConsumer 24 | 25 | typealias Predicate = JavaPredicate 26 | 27 | typealias Function = JavaFunction 28 | 29 | val alwaysTruePredicate = Predicate { true } 30 | 31 | typealias Mutable = Ref.ObjectRef 32 | 33 | typealias Atomic = AtomicReference 34 | 35 | fun mutable(value: T): Mutable { 36 | val result = Mutable() 37 | result.element = value 38 | return result 39 | } 40 | 41 | inline var Mutable.value: T 42 | get() = element 43 | set(value) { 44 | element = value 45 | } 46 | 47 | @Suppress("NOTHING_TO_INLINE") 48 | inline fun atomic(value: T): Atomic = 49 | Atomic(value) 50 | 51 | @Suppress("NOTHING_TO_INLINE") 52 | inline fun Atomic.cas(expectedValue: T, newValue: T): Boolean = 53 | compareAndSet(expectedValue, newValue) 54 | 55 | @Suppress("NOTHING_TO_INLINE") 56 | inline fun Atomic.exchange(newValue: T): T = 57 | getAndSet(newValue) 58 | 59 | inline var Atomic.value: T 60 | get() = get() 61 | set(value: T) { 62 | set(value) 63 | } 64 | 65 | @Suppress("NOTHING_TO_INLINE") 66 | inline operator fun Supplier.invoke(): T = 67 | get() 68 | 69 | @Suppress("NOTHING_TO_INLINE") 70 | inline operator fun Consumer.invoke(value: T) { 71 | accept(value) 72 | } 73 | 74 | @Suppress("NOTHING_TO_INLINE") 75 | inline operator fun Predicate.invoke(value: T): Boolean = 76 | test(value) 77 | 78 | @Suppress("NOTHING_TO_INLINE") 79 | inline operator fun Runnable.invoke() { 80 | run() 81 | } 82 | 83 | @Suppress("NOTHING_TO_INLINE") 84 | inline operator fun Function.invoke(input: INPUT): OUTPUT = 85 | apply(input) 86 | -------------------------------------------------------------------------------- /loomoroutines-common/src/main/kotlin/internal/kotlinstdlibstub/common-internal-kotlinstdlibstub-bytestreams.kt: -------------------------------------------------------------------------------- 1 | @file:JvmName("ByteStreamsKt") 2 | 3 | package dev.reformator.loomoroutines.common.internal.kotlinstdlibstub 4 | 5 | import java.io.ByteArrayOutputStream 6 | import java.io.InputStream 7 | import java.io.OutputStream 8 | 9 | fun InputStream.readBytes(): ByteArray { 10 | val buffer = ByteArrayOutputStream(maxOf(DEFAULT_BUFFER_SIZE, this.available())) 11 | copyTo(buffer) 12 | return buffer.toByteArray() 13 | } 14 | 15 | fun InputStream.copyTo(out: OutputStream, bufferSize: Int = DEFAULT_BUFFER_SIZE): Long { 16 | var bytesCopied: Long = 0 17 | val buffer = ByteArray(bufferSize) 18 | var bytes = read(buffer) 19 | while (bytes >= 0) { 20 | out.write(buffer, 0, bytes) 21 | bytesCopied += bytes 22 | bytes = read(buffer) 23 | } 24 | return bytesCopied 25 | } -------------------------------------------------------------------------------- /loomoroutines-common/src/main/kotlin/internal/kotlinstdlibstub/common-internal-kotlinstdlibstub-collections.kt: -------------------------------------------------------------------------------- 1 | @file:JvmName("CollectionsKt") 2 | 3 | package dev.reformator.loomoroutines.common.internal.kotlinstdlibstub 4 | 5 | import java.util.* 6 | 7 | fun listOf(element: T): List = Collections.singletonList(element) 8 | 9 | fun emptyList(): List = Collections.emptyList() 10 | 11 | @Suppress("EXTENSION_SHADOWED_BY_MEMBER") 12 | fun MutableList.removeLast(): T = if (isEmpty()) throw NoSuchElementException("List is empty.") else removeAt(lastIndex) 13 | 14 | val List.lastIndex: Int 15 | get() = this.size - 1 16 | 17 | fun Iterable.collectionSizeOrDefault(default: Int): Int = if (this is Collection<*>) this.size else default 18 | -------------------------------------------------------------------------------- /loomoroutines-common/src/main/kotlin/internal/kotlinstdlibstub/common-internal-kotlinstdlibstub-enums.kt: -------------------------------------------------------------------------------- 1 | @file:JvmName("EnumEntriesKt") 2 | 3 | package dev.reformator.loomoroutines.common.internal.kotlinstdlibstub 4 | 5 | import java.io.Serializable 6 | import java.util.AbstractList 7 | 8 | sealed interface EnumEntries> : List 9 | 10 | fun > enumEntries(entries: Array): EnumEntries = 11 | EnumEntriesList(entries) 12 | 13 | private class EnumEntriesList>(private val entries: Array): EnumEntries, AbstractList(), Serializable { 14 | override fun get(index: Int): T = 15 | entries[index] 16 | 17 | override val size: Int 18 | get() = entries.size 19 | } 20 | -------------------------------------------------------------------------------- /loomoroutines-common/src/main/kotlin/internal/kotlinstdlibstub/common-internal-kotlinstdlibstub-exeptions.kt: -------------------------------------------------------------------------------- 1 | package dev.reformator.loomoroutines.common.internal.kotlinstdlibstub 2 | 3 | class KotlinException : RuntimeException { 4 | constructor() : super() 5 | constructor(message: String?) : super(message) 6 | constructor(message: String?, cause: Throwable?) : super(message, cause) 7 | constructor(cause: Throwable?) : super(cause) 8 | } 9 | -------------------------------------------------------------------------------- /loomoroutines-common/src/main/kotlin/internal/kotlinstdlibstub/common-internal-kotlinstdlibstub-function.kt: -------------------------------------------------------------------------------- 1 | package dev.reformator.loomoroutines.common.internal.kotlinstdlibstub 2 | 3 | import java.io.Serializable 4 | 5 | interface Function 6 | 7 | interface FunctionBase: Function { 8 | val arity: Int 9 | } 10 | 11 | abstract class Lambda(override val arity: Int): FunctionBase, Serializable 12 | 13 | interface Function0 : Function { 14 | operator fun invoke(): R 15 | } 16 | 17 | interface Function1 : Function { 18 | operator fun invoke(p1: P1): R 19 | } 20 | 21 | interface Function2: Function { 22 | operator fun invoke(p1: P1, p2: P2): R 23 | } 24 | -------------------------------------------------------------------------------- /loomoroutines-common/src/main/kotlin/internal/kotlinstdlibstub/common-internal-kotlinstdlibstub-strings.kt: -------------------------------------------------------------------------------- 1 | @file:JvmName("StringsKt") 2 | 3 | package dev.reformator.loomoroutines.common.internal.kotlinstdlibstub 4 | 5 | @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") 6 | fun String.startsWith(prefix: String, ignoreCase: Boolean = false): Boolean = 7 | if (!ignoreCase) 8 | (this as java.lang.String).startsWith(prefix) 9 | else 10 | regionMatches(0, prefix, 0, prefix.length, ignoreCase) 11 | 12 | @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") 13 | fun String.regionMatches( 14 | thisOffset: Int, 15 | other: String, 16 | otherOffset: Int, 17 | length: Int, 18 | ignoreCase: Boolean = false 19 | ): Boolean = 20 | if (!ignoreCase) 21 | (this as java.lang.String).regionMatches(thisOffset, other, otherOffset, length) 22 | else 23 | (this as java.lang.String).regionMatches(ignoreCase, thisOffset, other, otherOffset, length) 24 | -------------------------------------------------------------------------------- /loomoroutines-common/src/main/kotlin/internal/kotlinstdlibstub/common-internal-kotlinstdlibstub-types.kt: -------------------------------------------------------------------------------- 1 | package dev.reformator.loomoroutines.common.internal.kotlinstdlibstub 2 | 3 | object Unit { 4 | override fun toString() = "kotlin.Unit-stub" 5 | } 6 | -------------------------------------------------------------------------------- /loomoroutines-common/src/test/java/module-info.java: -------------------------------------------------------------------------------- 1 | module tests { 2 | requires dev.reformator.loomoroutines.common; 3 | requires org.junit.jupiter.api; 4 | requires org.slf4j; 5 | requires org.apache.commons.lang3; 6 | 7 | exports tests; 8 | } 9 | -------------------------------------------------------------------------------- /loomoroutines-common/src/test/java/tests/CommonTest.java: -------------------------------------------------------------------------------- 1 | package tests; 2 | 3 | import dev.reformator.loomoroutines.common.CompletedCoroutine; 4 | import dev.reformator.loomoroutines.common.GeneratorUtils; 5 | import dev.reformator.loomoroutines.common.SuspendedCoroutine; 6 | import org.apache.commons.lang3.mutable.MutableBoolean; 7 | import org.apache.commons.lang3.mutable.MutableInt; 8 | import org.junit.jupiter.api.Assertions; 9 | import org.junit.jupiter.api.Test; 10 | 11 | import java.util.List; 12 | import java.util.NoSuchElementException; 13 | import java.util.Objects; 14 | 15 | import static dev.reformator.loomoroutines.common.CoroutineUtils.*; 16 | import static dev.reformator.loomoroutines.common.GeneratorUtils.*; 17 | import static org.junit.jupiter.api.Assertions.*; 18 | 19 | public class CommonTest { 20 | @Test 21 | public void testGenerators() { 22 | var iterator = loomIterator(scope -> { 23 | for (int i = 0; i < 10; i++) { 24 | scope.emit(i); 25 | } 26 | scope.emit(10); 27 | for (var value: loomIterable(innerScope -> { 28 | for (int i = 11; i < 20; i++) { 29 | if (i % 2 == 0) { 30 | innerScope.emit(i); 31 | } else { 32 | scope.emit(i); 33 | } 34 | } 35 | 36 | GeneratorUtils.loomStream((innerInnerScope) -> { 37 | for (int i = 20; i <= 30; i++) { 38 | if (i % 3 == 0) { 39 | innerInnerScope.emit(i); 40 | } else if (i % 3 == 1) { 41 | innerScope.emit(i); 42 | } else { 43 | scope.emit(i); 44 | } 45 | } 46 | }).forEach(i -> { 47 | if (i % 2 == 0) { 48 | innerScope.emit(i); 49 | } else { 50 | scope.emit(i); 51 | } 52 | }); 53 | })) { 54 | scope.emit(value); 55 | } 56 | }); 57 | 58 | for (int i = 0; i<= 30; i++) { 59 | assertTrue(iterator.hasNext()); 60 | assertEquals(i, iterator.next()); 61 | } 62 | assertFalse(iterator.hasNext()); 63 | assertThrows(NoSuchElementException.class, iterator::next); 64 | } 65 | 66 | @Test 67 | public void testCoroutineContextApi() { 68 | var context1CallCounter = new MutableInt(); 69 | createCoroutine("context1", () -> { 70 | context1CallCounter.increment(); 71 | 72 | assertEquals( 73 | List.of("context1"), 74 | getRunningCoroutinesContexts() 75 | ); 76 | assertEquals(1, getRunningCoroutinesNumber()); 77 | 78 | var callCounter = new MutableInt(); 79 | assertEquals("context1", getRunningCoroutineContext(context -> { 80 | callCounter.increment(); 81 | assertEquals("context1", context); 82 | return true; 83 | })); 84 | assertEquals(1, callCounter.intValue()); 85 | 86 | var context2CallCounter = new MutableInt(); 87 | createCoroutine("context2", () -> { 88 | context2CallCounter.increment(); 89 | 90 | assertEquals( 91 | List.of("context2", "context1"), 92 | getRunningCoroutinesContexts() 93 | ); 94 | assertEquals(2, getRunningCoroutinesNumber()); 95 | 96 | callCounter.setValue(0); 97 | assertEquals("context1", getRunningCoroutineContext(context -> { 98 | boolean result; 99 | if (callCounter.intValue() == 0) { 100 | assertEquals("context2", context); 101 | result = false; 102 | } else if (callCounter.intValue() == 1) { 103 | assertEquals("context1", context); 104 | result = true; 105 | } else { 106 | throw Assertions.fail("invalid callCounter: " + callCounter.intValue()); 107 | } 108 | callCounter.increment(); 109 | return result; 110 | })); 111 | assertEquals(2, callCounter.intValue()); 112 | 113 | var context3CallCounter = new MutableInt(); 114 | var coroutine3 = createCoroutine(List.of(120), () -> { 115 | context3CallCounter.increment(); 116 | 117 | callCounter.setValue(0); 118 | assertEquals( 119 | List.of(120), 120 | getRunningCoroutineContext(List.class, list -> { 121 | callCounter.increment(); 122 | assertEquals(120, list.get(0)); 123 | return true; 124 | }) 125 | ); 126 | assertEquals(1, callCounter.intValue()); 127 | 128 | assertEquals(List.of(120), getRunningCoroutineContext(List.class)); 129 | }); 130 | assertEquals(0, context3CallCounter.intValue()); 131 | assertEquals( 132 | List.of(120), 133 | assertInstanceOf(CompletedCoroutine.class, coroutine3.resume()).getCoroutineContext() 134 | ); 135 | assertEquals(1, context3CallCounter.intValue()); 136 | }).resume(); 137 | assertEquals(1, context2CallCounter.intValue()); 138 | }).resume(); 139 | assertEquals(1, context1CallCounter.intValue()); 140 | } 141 | 142 | @Test 143 | public void testCoroutineSuspensionApi() { 144 | var callCounter = new MutableInt(); 145 | var coroutine1 = createCoroutine("context1", () -> { 146 | assertEquals(1, callCounter.getAndIncrement()); 147 | assertFalse(trySuspendCoroutine(context -> { 148 | assertEquals(2, callCounter.getAndIncrement()); 149 | assertEquals("context1", context); 150 | return false; 151 | })); 152 | assertEquals(3, callCounter.getAndIncrement()); 153 | assertTrue(trySuspendCoroutine(context -> { 154 | assertEquals(4, callCounter.getAndIncrement()); 155 | assertEquals("context1", context); 156 | return true; 157 | })); 158 | assertEquals(6, callCounter.getAndIncrement()); 159 | }); 160 | assertEquals("context1", coroutine1.getCoroutineContext()); 161 | assertEquals(0, callCounter.getAndIncrement()); 162 | var coroutine2 = assertInstanceOf(SuspendedCoroutine.class, coroutine1.resume()); 163 | assertEquals("context1", coroutine2.getCoroutineContext()); 164 | assertEquals(5, callCounter.getAndIncrement()); 165 | var coroutine3 = assertInstanceOf(CompletedCoroutine.class, coroutine2.resume()); 166 | assertEquals(7, callCounter.getAndIncrement()); 167 | assertEquals("context1", coroutine3.getCoroutineContext()); 168 | } 169 | 170 | @Test 171 | public void testPinnedVirtualThread() throws InterruptedException { 172 | var completed =new MutableBoolean(false); 173 | Thread.startVirtualThread(() -> { 174 | synchronized (new Object()) { 175 | createCoroutine("context1", () -> { 176 | try { 177 | Thread.sleep(100); 178 | } catch (InterruptedException e) { 179 | throw new RuntimeException(e); 180 | } 181 | }).resume(); 182 | } 183 | completed.setTrue(); 184 | }).join(); 185 | assertTrue(completed.getValue()); 186 | } 187 | 188 | @Test 189 | public void testFailedSuspend() { 190 | try { 191 | createCoroutine("context1", () -> { 192 | synchronized (new Object()) { 193 | createCoroutine("context2", () -> { 194 | suspendCoroutine(c -> Objects.equals(c, "context1")); 195 | }).resume(); 196 | } 197 | }).resume(); 198 | } catch (IllegalStateException e) { 199 | assertTrue(e.getMessage().contains("Current thread is pinned")); 200 | return; 201 | } 202 | fail(); 203 | } 204 | 205 | @Test 206 | public void testNotFailedSuspend() { 207 | createCoroutine("context1", () -> { 208 | synchronized (new Object()) { 209 | createCoroutine("context2", () -> { 210 | suspendCoroutine(c -> Objects.equals(c, "context2")); 211 | }).resume(); 212 | } 213 | }).resume(); 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /loomoroutines-dispatcher/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.dokka.gradle.DokkaTask 2 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 3 | 4 | plugins { 5 | id("dev.reformator.javalibinkotlin") 6 | id("org.jetbrains.dokka") 7 | `maven-publish` 8 | signing 9 | } 10 | 11 | repositories { 12 | mavenCentral() 13 | } 14 | 15 | dependencies { 16 | compileOnly("org.jetbrains.kotlin:kotlin-stdlib:${kotlin.coreLibrariesVersion}") 17 | implementation(project(":loomoroutines-common")) 18 | implementation("org.slf4j:slf4j-api:${properties["slf4jVersion"]}") 19 | 20 | testImplementation("org.junit.jupiter:junit-jupiter-api:${properties["jupiterVersion"]}") 21 | testImplementation("org.apache.commons:commons-lang3:${properties["commonsLang3Version"]}") 22 | testRuntimeOnly("ch.qos.logback:logback-classic:${properties["logbackVersion"]}") 23 | testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${properties["jupiterVersion"]}") 24 | } 25 | 26 | java { 27 | sourceCompatibility = JavaVersion.VERSION_19 28 | targetCompatibility = JavaVersion.VERSION_19 29 | withSourcesJar() 30 | } 31 | 32 | kotlin { 33 | compilerOptions { 34 | jvmTarget = JvmTarget.JVM_19 35 | freeCompilerArgs.addAll("-Xjvm-default=all") 36 | } 37 | } 38 | 39 | sourceSets { 40 | main { 41 | kotlin.destinationDirectory = java.destinationDirectory 42 | } 43 | } 44 | 45 | tasks.test { 46 | useJUnitPlatform() 47 | jvmArgs("--add-exports", "java.base/jdk.internal.vm=dev.reformator.loomoroutines.common") 48 | } 49 | 50 | val javadocJarTask = tasks.create("javadocJar", Jar::class) { 51 | dependsOn("dokkaJavadoc") 52 | archiveClassifier = "javadoc" 53 | from(tasks.named("dokkaJavadoc").get().outputDirectory) 54 | } 55 | 56 | publishing { 57 | publications { 58 | create("maven") { 59 | from(components["java"]) 60 | artifact(javadocJarTask) 61 | pom { 62 | name.set("Loomoroutines Dispatchers lib.") 63 | description.set("Library for Java native coroutines dispatchers using Project Loom.") 64 | url.set("https://github.com/Anamorphosee/loomoroutines") 65 | licenses { 66 | license { 67 | name.set("The Apache License, Version 2.0") 68 | url.set("https://raw.githubusercontent.com/Anamorphosee/loomoroutines/main/LICENSE") 69 | } 70 | } 71 | developers { 72 | developer { 73 | name.set("Denis Berestinskii") 74 | email.set("berestinsky@gmail.com") 75 | url.set("https://github.com/Anamorphosee") 76 | } 77 | } 78 | scm { 79 | connection.set("scm:git:git://github.com/Anamorphosee/loomoroutines.git") 80 | developerConnection.set("scm:git:ssh://github.com:Anamorphosee/loomoroutines.git") 81 | url.set("http://github.com/Anamorphosee/loomoroutines/tree/main") 82 | } 83 | } 84 | } 85 | } 86 | repositories { 87 | maven { 88 | name = "sonatype" 89 | url = if (version.toString().endsWith("SNAPSHOT")) { 90 | uri("https://s01.oss.sonatype.org/content/repositories/snapshots/") 91 | } else { 92 | uri("https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/") 93 | } 94 | credentials { 95 | username = properties["sonatype.username"] as String? 96 | password = properties["sonatype.password"] as String? 97 | } 98 | } 99 | } 100 | } 101 | 102 | signing { 103 | useGpgCmd() 104 | sign(publishing.publications["maven"]) 105 | } 106 | -------------------------------------------------------------------------------- /loomoroutines-dispatcher/src/main/java/module-info.java: -------------------------------------------------------------------------------- 1 | module dev.reformator.loomoroutines.dispatcher { 2 | requires static kotlin.stdlib; 3 | requires static java.desktop; 4 | 5 | requires dev.reformator.loomoroutines.common; 6 | requires org.slf4j; 7 | 8 | exports dev.reformator.loomoroutines.dispatcher; 9 | } 10 | -------------------------------------------------------------------------------- /loomoroutines-dispatcher/src/main/kotlin/dispatcher-dispatcher.kt: -------------------------------------------------------------------------------- 1 | package dev.reformator.loomoroutines.dispatcher 2 | 3 | import dev.reformator.loomoroutines.common.internal.Consumer 4 | import java.time.Duration 5 | 6 | /** 7 | * A dispatcher, which can execute actions. 8 | */ 9 | interface Dispatcher { 10 | /** 11 | * Execute an [action] in the dispatcher. 12 | */ 13 | fun execute(action: Runnable) { 14 | scheduleExecute(Duration.ZERO, action) 15 | } 16 | 17 | /** 18 | * Execute an [action] in the dispatcher after a [delay]. 19 | * Used for a [dev.reformator.loomoroutines.dispatcher.delay] method. 20 | */ 21 | fun scheduleExecute(delay: Duration, action: Runnable) 22 | 23 | /** 24 | * Check, could an action be executed in the current thread instead of calling an [execute] method. 25 | * Used only for optimization, can always return `false`. 26 | */ 27 | fun canExecuteInCurrentThread(): Boolean = 28 | false 29 | } 30 | 31 | /** 32 | * A [Dispatcher] that have to be closed after usage. 33 | */ 34 | interface CloseableDispatcher: Dispatcher, AutoCloseable { 35 | /** 36 | * Close the dispatcher. 37 | */ 38 | override fun close() 39 | } 40 | 41 | /** 42 | * An asynchronous computation result. 43 | * @param T type of the result 44 | */ 45 | interface Promise { 46 | /** 47 | * State of the computation. 48 | */ 49 | val state: PromiseState 50 | 51 | /** 52 | * Get the result of the computation or throw an exception if the computation will fail. 53 | * Possibly blocking operation. Never blocks if it is called inside a dispatcher coroutine. 54 | */ 55 | fun join(): T 56 | 57 | /** 58 | * Add a [callback] that will be called after the computation will complete. 59 | * [callback] can be called inside the method if the [Promise] have already completed. 60 | */ 61 | fun subscribe(callback: Consumer>) 62 | } 63 | 64 | /** 65 | * A state of a [Promise] 66 | */ 67 | enum class PromiseState { 68 | /** 69 | * A [Promise] is not completed yet. 70 | */ 71 | RUNNING, 72 | 73 | /** 74 | * A [Promise] has completed successfully. 75 | */ 76 | COMPLETED, 77 | 78 | /** 79 | * A [Promise] has completed exceptionally. 80 | */ 81 | EXCEPTIONAL 82 | } 83 | 84 | /** 85 | * A completed [Promise] result. 86 | * Have to be one of [SucceedPromiseResult] or [ExceptionalPromiseResult]. 87 | * @param T the result's type 88 | */ 89 | sealed interface PromiseResult { 90 | /** 91 | * Get the result or throw an exception if the computation has failed. 92 | */ 93 | fun get(): T 94 | 95 | /** 96 | * Has the computation completed successfully. 97 | */ 98 | val succeed: Boolean 99 | } 100 | 101 | /** 102 | * A successfully completed [Promise] result. 103 | * @property result the result of the computation 104 | */ 105 | class SucceedPromiseResult(private val result: T): PromiseResult { 106 | override fun get(): T = 107 | result 108 | 109 | override val succeed: Boolean 110 | get() = true 111 | } 112 | 113 | /** 114 | * A [Promise] result completed exceptionally. 115 | * @property exception throws by the computation 116 | */ 117 | class ExceptionalPromiseResult(val exception: Throwable): PromiseResult { 118 | override fun get(): Nothing { 119 | throw exception 120 | } 121 | 122 | override val succeed: Boolean 123 | get() = false 124 | } 125 | -------------------------------------------------------------------------------- /loomoroutines-dispatcher/src/main/kotlin/dispatcher-dispatchers.kt: -------------------------------------------------------------------------------- 1 | package dev.reformator.loomoroutines.dispatcher 2 | 3 | import dev.reformator.loomoroutines.common.internal.error 4 | import dev.reformator.loomoroutines.common.internal.getLogger 5 | import dev.reformator.loomoroutines.common.internal.invoke 6 | import java.time.Duration 7 | import java.util.concurrent.Executors 8 | import java.util.concurrent.TimeUnit 9 | import javax.swing.SwingUtilities 10 | 11 | private val log = getLogger() 12 | 13 | /** 14 | * A [Dispatcher] that executes actions in virtual threads. 15 | */ 16 | @Suppress("Since15") 17 | object VirtualThreadsDispatcher: Dispatcher { 18 | private val exceptionHandler = Thread.UncaughtExceptionHandler { _, e -> 19 | log.error(e) { "Uncaught exception in a virtual thread dispatcher: ${e.message}" } 20 | } 21 | 22 | override fun execute(action: Runnable) { 23 | Thread.ofVirtual().uncaughtExceptionHandler(exceptionHandler).start(action) 24 | } 25 | 26 | override fun scheduleExecute(delay: Duration, action: Runnable) { 27 | Thread.ofVirtual().uncaughtExceptionHandler(exceptionHandler).start { 28 | Thread.sleep(delay) 29 | action() 30 | } 31 | } 32 | 33 | override fun canExecuteInCurrentThread(): Boolean = 34 | Thread.currentThread().isVirtual 35 | } 36 | 37 | /** 38 | * A [Dispatcher] that executes actions in a Swing event dispatcher thread. 39 | */ 40 | object SwingDispatcher: Dispatcher { 41 | private val scheduledExecutor = Executors.newSingleThreadScheduledExecutor() 42 | 43 | override fun execute(action: Runnable) { 44 | SwingUtilities.invokeLater { 45 | try { 46 | action() 47 | } catch (e: Throwable) { 48 | log.error(e) { "Uncaught exception in a UI thread dispatcher: ${e.message}" } 49 | } 50 | } 51 | } 52 | 53 | override fun scheduleExecute(delay: Duration, action: Runnable) { 54 | scheduledExecutor.schedule({ execute(action) }, delay.toMillis(), TimeUnit.MILLISECONDS) 55 | } 56 | 57 | override fun canExecuteInCurrentThread(): Boolean = 58 | SwingUtilities.isEventDispatchThread() 59 | } 60 | -------------------------------------------------------------------------------- /loomoroutines-dispatcher/src/main/kotlin/dispatcher-utils-kotlinapi.kt: -------------------------------------------------------------------------------- 1 | package dev.reformator.loomoroutines.dispatcher 2 | 3 | import dev.reformator.loomoroutines.common.internal.Consumer 4 | import dev.reformator.loomoroutines.common.internal.Supplier 5 | import kotlin.time.Duration 6 | import kotlin.time.toJavaDuration 7 | 8 | @CallOnlyInDispatcher 9 | inline fun await(crossinline callback: (Notifier) -> Unit) { 10 | await(Consumer { callback(it) }) 11 | } 12 | 13 | @CallOnlyInDispatcher 14 | fun delay(duration: Duration) { 15 | delay(duration.toJavaDuration()) 16 | } 17 | 18 | @CallOnlyInDispatcher 19 | inline fun doIn(dispatcher: Dispatcher, crossinline action: () -> T): T = 20 | doIn(dispatcher, Supplier { action() }) 21 | 22 | inline fun Dispatcher.dispatch(crossinline body: () -> T): Promise = 23 | dispatch(Supplier { body() }) 24 | -------------------------------------------------------------------------------- /loomoroutines-dispatcher/src/main/kotlin/dispatcher-utils.kt: -------------------------------------------------------------------------------- 1 | @file:JvmName("DispatcherUtils") 2 | 3 | package dev.reformator.loomoroutines.dispatcher 4 | 5 | import dev.reformator.loomoroutines.common.createCoroutine 6 | import dev.reformator.loomoroutines.common.getRunningCoroutineContext 7 | import dev.reformator.loomoroutines.common.internal.Consumer 8 | import dev.reformator.loomoroutines.common.internal.Supplier 9 | import dev.reformator.loomoroutines.common.internal.invoke 10 | import dev.reformator.loomoroutines.common.internal.kotlinstdlibstub.Ref 11 | import dev.reformator.loomoroutines.common.trySuspendCoroutine 12 | import dev.reformator.loomoroutines.dispatcher.internal.DispatcherContext 13 | import dev.reformator.loomoroutines.dispatcher.internal.DispatcherContextImpl 14 | import dev.reformator.loomoroutines.dispatcher.internal.ScheduledExecutorServiceDispatcher 15 | import dev.reformator.loomoroutines.dispatcher.internal.dispatch 16 | import java.time.Duration 17 | import java.util.concurrent.ScheduledExecutorService 18 | 19 | /** 20 | * Optional annotation to warn that the method must be called only inside a dispatcher coroutine. 21 | */ 22 | @Target(AnnotationTarget.FUNCTION) 23 | annotation class CallOnlyInDispatcher 24 | 25 | /** 26 | * Is current thread running in a dispatcher coroutine. 27 | */ 28 | val isInDispatcher: Boolean 29 | get() = getRunningCoroutineContext>() != null 30 | 31 | /** 32 | * Notifier about completing of an awaiting. 33 | * @see await 34 | */ 35 | fun interface Notifier { 36 | /** 37 | * Notify the dispatcher coroutine that the awaiting has completed. 38 | * Must be called only once for each [Notifier]. 39 | * @see await 40 | */ 41 | operator fun invoke() 42 | } 43 | 44 | /** 45 | * Suspend a dispatcher coroutine until a [Notifier] will be invoked. 46 | * The method suspend a dispatcher coroutine and call [callback] with a [Notifier] which must be invoked to continue the coroutine. 47 | */ 48 | @CallOnlyInDispatcher 49 | fun await(callback: Consumer) { 50 | sendDispatcherEvent { 51 | setAwaitLastEvent(callback) 52 | } 53 | } 54 | 55 | /** 56 | * Suspend a dispatcher coroutine and continue it after a [delay][duration]. 57 | */ 58 | @CallOnlyInDispatcher 59 | fun delay(duration: Duration) { 60 | sendDispatcherEvent { 61 | setDelayLastEvent(duration) 62 | } 63 | } 64 | 65 | /** 66 | * Execute an [action] possibly switching a [Dispatcher]. 67 | * @param dispatcher a dispatcher in which [action] will be executed 68 | * @return [action]'s result 69 | */ 70 | @CallOnlyInDispatcher 71 | fun doIn(dispatcher: Dispatcher, action: Supplier): T { 72 | val context = getMandatoryRunningCoroutineDispatcherContext() 73 | val oldDispatcher = context.dispatcher!! 74 | if (oldDispatcher === dispatcher) { 75 | return action() 76 | } else { 77 | sendDispatcherEvent { 78 | setSwitchLastEvent(dispatcher) 79 | } 80 | val result = try { 81 | action() 82 | } finally { 83 | sendDispatcherEvent { 84 | setSwitchLastEvent(oldDispatcher) 85 | } 86 | } 87 | return result 88 | } 89 | } 90 | 91 | /** 92 | * Create a dispatcher coroutine that will run in [this] and execute [body]. 93 | */ 94 | fun Dispatcher.dispatch(body: Supplier): Promise { 95 | val context = DispatcherContextImpl() 96 | val result = Ref.ObjectRef() 97 | val coroutine = createCoroutine(context) { result.element = body() } 98 | dispatch(coroutine, result) 99 | return context.promise 100 | } 101 | 102 | /** 103 | * Create a [CloseableDispatcher] that executes actions in [this]. 104 | */ 105 | fun ScheduledExecutorService.toDispatcher(): CloseableDispatcher = 106 | ScheduledExecutorServiceDispatcher(this) 107 | 108 | @CallOnlyInDispatcher 109 | private inline fun sendDispatcherEvent(crossinline generateEvent: DispatcherContext<*>.() -> Unit) { 110 | val suspended = trySuspendCoroutine> { 111 | it.generateEvent() 112 | true 113 | } 114 | if (!suspended) { 115 | throwNotInDispatcher() 116 | } 117 | } 118 | 119 | @CallOnlyInDispatcher 120 | private fun getMandatoryRunningCoroutineDispatcherContext(): DispatcherContext<*> = 121 | getRunningCoroutineContext>() ?: throwNotInDispatcher() 122 | 123 | @CallOnlyInDispatcher 124 | private fun throwNotInDispatcher(): Nothing = 125 | error("Method must be called in a dispatcher coroutine.") 126 | -------------------------------------------------------------------------------- /loomoroutines-dispatcher/src/main/kotlin/internal/disparcher-internal-context.kt: -------------------------------------------------------------------------------- 1 | package dev.reformator.loomoroutines.dispatcher.internal 2 | 3 | import dev.reformator.loomoroutines.common.internal.Consumer 4 | import dev.reformator.loomoroutines.dispatcher.Dispatcher 5 | import dev.reformator.loomoroutines.dispatcher.Promise 6 | import dev.reformator.loomoroutines.dispatcher.PromiseResult 7 | import dev.reformator.loomoroutines.dispatcher.Notifier 8 | import java.time.Duration 9 | 10 | internal interface DispatcherContext { 11 | val promise: Promise 12 | 13 | var dispatcher: Dispatcher? 14 | 15 | val lastEvent: DispatcherEvent 16 | 17 | fun setAwaitLastEvent(callback: Consumer) 18 | 19 | fun setDelayLastEvent(duration: Duration) 20 | 21 | fun setSwitchLastEvent(newDispatcher: Dispatcher) 22 | 23 | fun complete(result: PromiseResult) 24 | } 25 | 26 | internal sealed interface DispatcherEvent 27 | 28 | internal class AwaitDispatcherEvent(val callback: Consumer): DispatcherEvent 29 | 30 | internal class DelayDispatcherEvent(val duration: Duration): DispatcherEvent 31 | 32 | internal class SwitchDispatcherEvent(val newDispatcher: Dispatcher): DispatcherEvent 33 | -------------------------------------------------------------------------------- /loomoroutines-dispatcher/src/main/kotlin/internal/dispatcher-internal-context-impl.kt: -------------------------------------------------------------------------------- 1 | package dev.reformator.loomoroutines.dispatcher.internal 2 | 3 | import dev.reformator.loomoroutines.common.internal.* 4 | import dev.reformator.loomoroutines.dispatcher.* 5 | import java.time.Duration 6 | import java.util.concurrent.Semaphore 7 | 8 | private val log = getLogger() 9 | 10 | internal class DispatcherContextImpl: DispatcherContext, Promise { 11 | private val _state = atomic>(EmptyRunningDispatcherContextImplState) 12 | private val _lastEvent = atomic(null) 13 | 14 | override val state: PromiseState 15 | get() = _state.value.state 16 | 17 | override fun join(): T { 18 | //Completed 19 | _state.value.let { state -> 20 | if (state is CompletedDispatcherContextImplState) { 21 | return state.result.get() 22 | } 23 | } 24 | 25 | //In dispatcher coroutine 26 | if (isInDispatcher) { 27 | var result: PromiseResult? = null 28 | await { notifier -> 29 | subscribe { 30 | result = it 31 | notifier() 32 | } 33 | } 34 | return result!!.get() 35 | } 36 | 37 | //Blocking 38 | run { 39 | val semaphore = Semaphore(0) 40 | var result: PromiseResult? = null 41 | subscribe { 42 | result = it 43 | semaphore.release() 44 | } 45 | semaphore.acquire() 46 | return result!!.get() 47 | } 48 | } 49 | 50 | override fun subscribe(callback: Consumer>) { 51 | loop { 52 | val state = _state.value 53 | if ( 54 | state is RunningDispatcherContextImplState && 55 | _state.cas(state, NotEmptyRunningDispatcherContextImplState(callback, state)) 56 | ) { 57 | return 58 | } 59 | if (state is CompletedDispatcherContextImplState) { 60 | callCallback(callback, state.result) 61 | return 62 | } 63 | } 64 | } 65 | 66 | override val promise: Promise 67 | get() = this 68 | 69 | override var dispatcher: Dispatcher? = null 70 | 71 | override val lastEvent: DispatcherEvent 72 | get() = _lastEvent.exchange(null) ?: error("Last event is not set.") 73 | 74 | override fun setAwaitLastEvent(callback: Consumer) { 75 | setLastEvent(AwaitDispatcherEvent(callback)) 76 | } 77 | 78 | override fun setDelayLastEvent(duration: Duration) { 79 | setLastEvent(DelayDispatcherEvent(duration)) 80 | } 81 | 82 | override fun setSwitchLastEvent(newDispatcher: Dispatcher) { 83 | setLastEvent(SwitchDispatcherEvent(newDispatcher)) 84 | } 85 | 86 | override fun complete(result: PromiseResult) { 87 | loop { 88 | var state = _state.value 89 | if (state is RunningDispatcherContextImplState) { 90 | if (_state.cas(state, CompletedDispatcherContextImplState(result))) { 91 | while (state is NotEmptyRunningDispatcherContextImplState) { 92 | callCallback(state.callback, result) 93 | state = state.next 94 | } 95 | assert { state == EmptyRunningDispatcherContextImplState } 96 | return 97 | } 98 | } 99 | if (state is CompletedDispatcherContextImplState) { 100 | error("Dispatcher context is already completed.") 101 | } 102 | } 103 | } 104 | 105 | private fun setLastEvent(event: DispatcherEvent) { 106 | if (!_lastEvent.cas(null, event)) { 107 | error("Last event is already set") 108 | } 109 | } 110 | } 111 | 112 | private sealed interface DispatcherContextImplState { 113 | val state: PromiseState 114 | } 115 | 116 | private sealed interface RunningDispatcherContextImplState: DispatcherContextImplState { 117 | override val state: PromiseState 118 | get() = PromiseState.RUNNING 119 | } 120 | 121 | private data object EmptyRunningDispatcherContextImplState: RunningDispatcherContextImplState 122 | 123 | private class NotEmptyRunningDispatcherContextImplState( 124 | val callback: Consumer>, 125 | val next: RunningDispatcherContextImplState 126 | ): RunningDispatcherContextImplState 127 | 128 | private class CompletedDispatcherContextImplState(val result: PromiseResult): DispatcherContextImplState { 129 | override val state: PromiseState 130 | get() = if (result.succeed) PromiseState.COMPLETED else PromiseState.EXCEPTIONAL 131 | } 132 | 133 | private fun callCallback(callback: Consumer, result: T) { 134 | try { 135 | callback(result) 136 | } catch (e: Throwable) { 137 | log.error(e) { "Callback [$callback] failed." } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /loomoroutines-dispatcher/src/main/kotlin/internal/dispatcher-internal-loop.kt: -------------------------------------------------------------------------------- 1 | package dev.reformator.loomoroutines.dispatcher.internal 2 | 3 | import dev.reformator.loomoroutines.common.CompletedCoroutine 4 | import dev.reformator.loomoroutines.common.SuspendedCoroutine 5 | import dev.reformator.loomoroutines.common.internal.error 6 | import dev.reformator.loomoroutines.common.internal.getLogger 7 | import dev.reformator.loomoroutines.common.internal.invoke 8 | import dev.reformator.loomoroutines.common.internal.kotlinstdlibstub.Ref 9 | import dev.reformator.loomoroutines.dispatcher.Dispatcher 10 | import dev.reformator.loomoroutines.dispatcher.ExceptionalPromiseResult 11 | import dev.reformator.loomoroutines.dispatcher.SucceedPromiseResult 12 | import dev.reformator.loomoroutines.dispatcher.Notifier 13 | import java.util.concurrent.atomic.AtomicBoolean 14 | 15 | private val log = getLogger() 16 | 17 | internal fun Dispatcher.dispatch(coroutine: SuspendedCoroutine>, result: Ref.ObjectRef) { 18 | if (canExecuteInCurrentThread()) { 19 | dispatchInCurrentThread(coroutine, result) 20 | } else { 21 | execute { dispatchInCurrentThread(coroutine, result) } 22 | } 23 | } 24 | 25 | private fun Dispatcher.dispatchInCurrentThread( 26 | coroutine: SuspendedCoroutine>, 27 | result: Ref.ObjectRef 28 | ) { 29 | try { 30 | val context = coroutine.coroutineContext 31 | context.dispatcher = this 32 | val nextPoint = try { 33 | coroutine.resume() 34 | } catch (e: Throwable) { 35 | context.dispatcher = null 36 | context.complete(ExceptionalPromiseResult(e)) 37 | return 38 | } 39 | context.dispatcher = null 40 | when (nextPoint) { 41 | is SuspendedCoroutine> -> { 42 | when (val event = context.lastEvent) { 43 | is AwaitDispatcherEvent -> { 44 | val awakened = AtomicBoolean(false) 45 | event.callback(Notifier { 46 | if (awakened.compareAndSet(false, true)) { 47 | dispatch(nextPoint, result) 48 | } else { 49 | error("Notifier is already invoked.") 50 | } 51 | }) 52 | } 53 | 54 | is DelayDispatcherEvent -> scheduleExecute(event.duration) { 55 | dispatchInCurrentThread(nextPoint, result) 56 | } 57 | 58 | is SwitchDispatcherEvent -> event.newDispatcher.dispatch(nextPoint, result) 59 | } 60 | } 61 | is CompletedCoroutine> -> context.complete(SucceedPromiseResult(result.element)) 62 | } 63 | } catch (e: Throwable) { 64 | log.error(e) { "Failed executing dispatcher event." } 65 | throw e 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /loomoroutines-dispatcher/src/main/kotlin/internal/dispatcher-internal-utils.kt: -------------------------------------------------------------------------------- 1 | package dev.reformator.loomoroutines.dispatcher.internal 2 | 3 | import dev.reformator.loomoroutines.dispatcher.CloseableDispatcher 4 | import java.time.Duration 5 | import java.util.concurrent.ScheduledExecutorService 6 | import java.util.concurrent.TimeUnit 7 | 8 | internal class ScheduledExecutorServiceDispatcher(private val executor: ScheduledExecutorService): CloseableDispatcher { 9 | override fun execute(action: Runnable) { 10 | executor.execute(action) 11 | } 12 | 13 | override fun scheduleExecute(delay: Duration, action: Runnable) { 14 | executor.schedule(action, delay.toMillis(), TimeUnit.MILLISECONDS) 15 | } 16 | 17 | override fun close() { 18 | executor.close() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /loomoroutines-dispatcher/src/test/java/module-info.java: -------------------------------------------------------------------------------- 1 | module tests { 2 | requires dev.reformator.loomoroutines.dispatcher; 3 | requires org.junit.jupiter.api; 4 | requires org.slf4j; 5 | requires org.apache.commons.lang3; 6 | 7 | exports tests; 8 | } 9 | -------------------------------------------------------------------------------- /loomoroutines-dispatcher/src/test/java/tests/DispatcherTest.java: -------------------------------------------------------------------------------- 1 | package tests; 2 | 3 | import dev.reformator.loomoroutines.dispatcher.DispatcherUtils; 4 | import dev.reformator.loomoroutines.dispatcher.PromiseState; 5 | import dev.reformator.loomoroutines.dispatcher.VirtualThreadsDispatcher; 6 | import org.apache.commons.lang3.mutable.MutableInt; 7 | import org.junit.jupiter.api.Assertions; 8 | import org.junit.jupiter.api.Test; 9 | 10 | import java.time.Duration; 11 | import java.util.List; 12 | import java.util.concurrent.Executors; 13 | 14 | @SuppressWarnings("Since15") 15 | public class DispatcherTest { 16 | @Test 17 | public void testDispatcherApi() { 18 | Assertions.assertFalse(DispatcherUtils.isInDispatcher()); 19 | var callCounter = new MutableInt(); 20 | var promise = DispatcherUtils.dispatch(VirtualThreadsDispatcher.INSTANCE, () -> { 21 | Assertions.assertEquals(0, callCounter.getAndIncrement()); 22 | Assertions.assertTrue(Thread.currentThread().isVirtual()); 23 | Assertions.assertTrue(DispatcherUtils.isInDispatcher()); 24 | DispatcherUtils.await(notifier -> { 25 | Assertions.assertEquals(1, callCounter.getAndIncrement()); 26 | try { 27 | Thread.sleep(100); 28 | } catch (InterruptedException e) { 29 | throw new RuntimeException(e); 30 | } 31 | Assertions.assertEquals(2, callCounter.getAndIncrement()); 32 | notifier.invoke(); 33 | }); 34 | Assertions.assertEquals(3, callCounter.getAndIncrement()); 35 | Assertions.assertTrue(Thread.currentThread().isVirtual()); 36 | Assertions.assertTrue(DispatcherUtils.isInDispatcher()); 37 | var currentTime = System.currentTimeMillis(); 38 | DispatcherUtils.delay(Duration.ofMillis(100)); 39 | Assertions.assertTrue(System.currentTimeMillis() >= currentTime + 100); 40 | Assertions.assertEquals(4, callCounter.getAndIncrement()); 41 | Assertions.assertTrue(Thread.currentThread().isVirtual()); 42 | Assertions.assertTrue(DispatcherUtils.isInDispatcher()); 43 | Object doInResult; 44 | try (var dispatcher = DispatcherUtils.toDispatcher(Executors.newSingleThreadScheduledExecutor())) { 45 | doInResult = DispatcherUtils.doIn(dispatcher, () -> { 46 | Assertions.assertEquals(5, callCounter.getAndIncrement()); 47 | Assertions.assertFalse(Thread.currentThread().isVirtual()); 48 | Assertions.assertTrue(DispatcherUtils.isInDispatcher()); 49 | var innerCurrentTime = System.currentTimeMillis(); 50 | DispatcherUtils.delay(Duration.ofMillis(100)); 51 | Assertions.assertTrue(System.currentTimeMillis() >= innerCurrentTime + 100); 52 | Assertions.assertEquals(6, callCounter.getAndIncrement()); 53 | Assertions.assertFalse(Thread.currentThread().isVirtual()); 54 | Assertions.assertTrue(DispatcherUtils.isInDispatcher()); 55 | return "result"; 56 | }); 57 | } 58 | Assertions.assertEquals(7, callCounter.getAndIncrement()); 59 | Assertions.assertTrue(Thread.currentThread().isVirtual()); 60 | Assertions.assertTrue(DispatcherUtils.isInDispatcher()); 61 | Assertions.assertEquals("result", doInResult); 62 | return List.of(123); 63 | }); 64 | Assertions.assertEquals(PromiseState.RUNNING, promise.getState()); 65 | Assertions.assertFalse(DispatcherUtils.isInDispatcher()); 66 | var list = promise.join(); 67 | Assertions.assertEquals(8, callCounter.getAndIncrement()); 68 | Assertions.assertEquals(PromiseState.COMPLETED, promise.getState()); 69 | Assertions.assertFalse(DispatcherUtils.isInDispatcher()); 70 | Assertions.assertEquals(List.of(123), list); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "loomoroutines" 2 | include( 3 | "loomoroutines-common", 4 | "loomoroutines-dispatcher", 5 | "loomoroutines-bypassjpms", 6 | "tests-kotlin", 7 | "tests-bypassjpms-nomodule", 8 | "tests-nomodule" 9 | ) 10 | -------------------------------------------------------------------------------- /tests-bypassjpms-nomodule/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `java-library` 3 | } 4 | 5 | repositories { 6 | mavenCentral() 7 | } 8 | 9 | dependencies { 10 | testImplementation(project(":loomoroutines-common")) 11 | testRuntimeOnly(project(":loomoroutines-bypassjpms")) 12 | testImplementation("org.junit.jupiter:junit-jupiter-api:${properties["jupiterVersion"]}") 13 | testImplementation("org.apache.commons:commons-lang3:${properties["commonsLang3Version"]}") 14 | testRuntimeOnly("ch.qos.logback:logback-classic:${properties["logbackVersion"]}") 15 | testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${properties["jupiterVersion"]}") 16 | } 17 | 18 | java { 19 | sourceCompatibility = JavaVersion.VERSION_21 20 | targetCompatibility = JavaVersion.VERSION_21 21 | } 22 | 23 | tasks.test { 24 | useJUnitPlatform() 25 | } 26 | -------------------------------------------------------------------------------- /tests-bypassjpms-nomodule/src/test/java/BypassJpmsNomoduleTest.java: -------------------------------------------------------------------------------- 1 | import dev.reformator.loomoroutines.common.CompletedCoroutine; 2 | import dev.reformator.loomoroutines.common.GeneratorUtils; 3 | import dev.reformator.loomoroutines.common.SuspendedCoroutine; 4 | import org.apache.commons.lang3.mutable.MutableBoolean; 5 | import org.apache.commons.lang3.mutable.MutableInt; 6 | import org.junit.jupiter.api.Assertions; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import java.util.List; 10 | import java.util.NoSuchElementException; 11 | import java.util.Objects; 12 | 13 | import static dev.reformator.loomoroutines.common.CoroutineUtils.*; 14 | import static dev.reformator.loomoroutines.common.GeneratorUtils.*; 15 | import static org.junit.jupiter.api.Assertions.*; 16 | 17 | public class BypassJpmsNomoduleTest { 18 | @Test 19 | public void testGenerators() { 20 | var iterator = loomIterator(scope -> { 21 | for (int i = 0; i < 10; i++) { 22 | scope.emit(i); 23 | } 24 | scope.emit(10); 25 | for (var value: loomIterable(innerScope -> { 26 | for (int i = 11; i < 20; i++) { 27 | if (i % 2 == 0) { 28 | innerScope.emit(i); 29 | } else { 30 | scope.emit(i); 31 | } 32 | } 33 | 34 | GeneratorUtils.loomStream((innerInnerScope) -> { 35 | for (int i = 20; i <= 30; i++) { 36 | if (i % 3 == 0) { 37 | innerInnerScope.emit(i); 38 | } else if (i % 3 == 1) { 39 | innerScope.emit(i); 40 | } else { 41 | scope.emit(i); 42 | } 43 | } 44 | }).forEach(i -> { 45 | if (i % 2 == 0) { 46 | innerScope.emit(i); 47 | } else { 48 | scope.emit(i); 49 | } 50 | }); 51 | })) { 52 | scope.emit(value); 53 | } 54 | }); 55 | 56 | for (int i = 0; i<= 30; i++) { 57 | assertTrue(iterator.hasNext()); 58 | assertEquals(i, iterator.next()); 59 | } 60 | assertFalse(iterator.hasNext()); 61 | assertThrows(NoSuchElementException.class, iterator::next); 62 | } 63 | 64 | @Test 65 | public void testCoroutineContextApi() { 66 | var context1CallCounter = new MutableInt(); 67 | createCoroutine("context1", () -> { 68 | context1CallCounter.increment(); 69 | 70 | assertEquals( 71 | List.of("context1"), 72 | getRunningCoroutinesContexts() 73 | ); 74 | assertEquals(1, getRunningCoroutinesNumber()); 75 | 76 | var callCounter = new MutableInt(); 77 | assertEquals("context1", getRunningCoroutineContext(context -> { 78 | callCounter.increment(); 79 | assertEquals("context1", context); 80 | return true; 81 | })); 82 | assertEquals(1, callCounter.intValue()); 83 | 84 | var context2CallCounter = new MutableInt(); 85 | createCoroutine("context2", () -> { 86 | context2CallCounter.increment(); 87 | 88 | assertEquals( 89 | List.of("context2", "context1"), 90 | getRunningCoroutinesContexts() 91 | ); 92 | assertEquals(2, getRunningCoroutinesNumber()); 93 | 94 | callCounter.setValue(0); 95 | assertEquals("context1", getRunningCoroutineContext(context -> { 96 | boolean result; 97 | if (callCounter.intValue() == 0) { 98 | assertEquals("context2", context); 99 | result = false; 100 | } else if (callCounter.intValue() == 1) { 101 | assertEquals("context1", context); 102 | result = true; 103 | } else { 104 | throw Assertions.fail("invalid callCounter: " + callCounter.intValue()); 105 | } 106 | callCounter.increment(); 107 | return result; 108 | })); 109 | assertEquals(2, callCounter.intValue()); 110 | 111 | var context3CallCounter = new MutableInt(); 112 | var coroutine3 = createCoroutine(List.of(120), () -> { 113 | context3CallCounter.increment(); 114 | 115 | callCounter.setValue(0); 116 | assertEquals( 117 | List.of(120), 118 | getRunningCoroutineContext(List.class, list -> { 119 | callCounter.increment(); 120 | assertEquals(120, list.get(0)); 121 | return true; 122 | }) 123 | ); 124 | assertEquals(1, callCounter.intValue()); 125 | 126 | assertEquals(List.of(120), getRunningCoroutineContext(List.class)); 127 | }); 128 | assertEquals(0, context3CallCounter.intValue()); 129 | assertEquals( 130 | List.of(120), 131 | assertInstanceOf(CompletedCoroutine.class, coroutine3.resume()).getCoroutineContext() 132 | ); 133 | assertEquals(1, context3CallCounter.intValue()); 134 | }).resume(); 135 | assertEquals(1, context2CallCounter.intValue()); 136 | }).resume(); 137 | assertEquals(1, context1CallCounter.intValue()); 138 | } 139 | 140 | @Test 141 | public void testCoroutineSuspensionApi() { 142 | var callCounter = new MutableInt(); 143 | var coroutine1 = createCoroutine("context1", () -> { 144 | assertEquals(1, callCounter.getAndIncrement()); 145 | assertFalse(trySuspendCoroutine(context -> { 146 | assertEquals(2, callCounter.getAndIncrement()); 147 | assertEquals("context1", context); 148 | return false; 149 | })); 150 | assertEquals(3, callCounter.getAndIncrement()); 151 | assertTrue(trySuspendCoroutine(context -> { 152 | assertEquals(4, callCounter.getAndIncrement()); 153 | assertEquals("context1", context); 154 | return true; 155 | })); 156 | assertEquals(6, callCounter.getAndIncrement()); 157 | }); 158 | assertEquals("context1", coroutine1.getCoroutineContext()); 159 | assertEquals(0, callCounter.getAndIncrement()); 160 | var coroutine2 = assertInstanceOf(SuspendedCoroutine.class, coroutine1.resume()); 161 | assertEquals("context1", coroutine2.getCoroutineContext()); 162 | assertEquals(5, callCounter.getAndIncrement()); 163 | var coroutine3 = assertInstanceOf(CompletedCoroutine.class, coroutine2.resume()); 164 | assertEquals(7, callCounter.getAndIncrement()); 165 | assertEquals("context1", coroutine3.getCoroutineContext()); 166 | } 167 | 168 | @Test 169 | public void testPinnedVirtualThread() throws InterruptedException { 170 | var completed =new MutableBoolean(false); 171 | Thread.startVirtualThread(() -> { 172 | synchronized (new Object()) { 173 | createCoroutine("context1", () -> { 174 | try { 175 | Thread.sleep(100); 176 | } catch (InterruptedException e) { 177 | throw new RuntimeException(e); 178 | } 179 | }).resume(); 180 | } 181 | completed.setTrue(); 182 | }).join(); 183 | assertTrue(completed.getValue()); 184 | } 185 | 186 | @Test 187 | public void testFailedSuspend() { 188 | try { 189 | createCoroutine("context1", () -> { 190 | synchronized (new Object()) { 191 | createCoroutine("context2", () -> { 192 | suspendCoroutine(c -> Objects.equals(c, "context1")); 193 | }).resume(); 194 | } 195 | }).resume(); 196 | } catch (IllegalStateException e) { 197 | assertTrue(e.getMessage().contains("Current thread is pinned")); 198 | return; 199 | } 200 | fail(); 201 | } 202 | 203 | @Test 204 | public void testNotFailedSuspend() { 205 | createCoroutine("context1", () -> { 206 | synchronized (new Object()) { 207 | createCoroutine("context2", () -> { 208 | suspendCoroutine(c -> Objects.equals(c, "context2")); 209 | }).resume(); 210 | } 211 | }).resume(); 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /tests-kotlin/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 2 | 3 | plugins { 4 | `java-library` 5 | kotlin("jvm") 6 | } 7 | 8 | repositories { 9 | mavenCentral() 10 | } 11 | 12 | dependencies { 13 | testImplementation(project(":loomoroutines-common")) 14 | testImplementation(project(":loomoroutines-dispatcher")) 15 | testImplementation(kotlin("test")) 16 | testImplementation("io.github.microutils:kotlin-logging-jvm:${properties["kotlinLoggingVersion"]}") 17 | testRuntimeOnly("ch.qos.logback:logback-classic:${properties["logbackVersion"]}") 18 | testImplementation("org.junit.jupiter:junit-jupiter:${properties["jupiterVersion"]}") 19 | testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:${properties["kotlinxCoroutinesVersion"]}") 20 | } 21 | 22 | kotlin { 23 | compilerOptions { 24 | jvmTarget = JvmTarget.JVM_21 25 | } 26 | } 27 | 28 | java { 29 | sourceCompatibility = JavaVersion.VERSION_21 30 | targetCompatibility = JavaVersion.VERSION_21 31 | } 32 | 33 | tasks.test { 34 | useJUnitPlatform() 35 | jvmArgs("--add-exports", "java.base/jdk.internal.vm=dev.reformator.loomoroutines.common") 36 | } 37 | 38 | -------------------------------------------------------------------------------- /tests-kotlin/src/test/java/module-info.java: -------------------------------------------------------------------------------- 1 | module tests { 2 | requires dev.reformator.loomoroutines.common; 3 | requires dev.reformator.loomoroutines.dispatcher; 4 | requires io.github.microutils.kotlinlogging; 5 | requires kotlin.stdlib; 6 | requires kotlin.test.junit5; 7 | requires kotlinx.coroutines.core; 8 | } 9 | -------------------------------------------------------------------------------- /tests-kotlin/src/test/kotlin/common-test-coroutines.kt: -------------------------------------------------------------------------------- 1 | import dev.reformator.loomoroutines.common.loomIterator 2 | import dev.reformator.loomoroutines.dispatcher.* 3 | import kotlin.coroutines.suspendCoroutine as kotlinSuspendCoroutine 4 | import kotlinx.coroutines.* 5 | import kotlinx.coroutines.delay as kotlinDelay 6 | import kotlinx.coroutines.future.asCompletableFuture 7 | import mu.KotlinLogging 8 | import java.util.concurrent.Executors 9 | import kotlin.coroutines.resume 10 | import kotlin.test.Test 11 | import kotlin.time.Duration 12 | import kotlin.time.Duration.Companion.milliseconds 13 | import kotlin.time.TimedValue 14 | import kotlin.time.measureTimedValue 15 | 16 | val log = KotlinLogging.logger { } 17 | 18 | class TestCoroutinePerformance { 19 | @Test 20 | fun fibonacciIterator() { 21 | System.setProperty("marker", "fibonacciIterator-kotlin") 22 | val kotlinIter = iterator { 23 | generateFibonacci { yield(it) } 24 | } 25 | doMeasurement(action = kotlinIter::next) 26 | 27 | System.setProperty("marker", "fibonacciIterator-loomoroutines") 28 | val loomIter = loomIterator { 29 | generateFibonacci(::emit) 30 | } 31 | doMeasurement(action = loomIter::next) 32 | } 33 | 34 | @Test 35 | fun dispatchersSwitch() { 36 | System.setProperty("marker", "dispatchersSwitch-kotlin") 37 | Executors.newSingleThreadScheduledExecutor().asCoroutineDispatcher().use { dispatcher1 -> 38 | Executors.newSingleThreadScheduledExecutor().asCoroutineDispatcher().use { dispatcher2 -> 39 | val scope = CoroutineScope(dispatcher1) 40 | doMeasurement { 41 | scope.launch { 42 | log.debug { "in dispatcher 1" } 43 | withContext(dispatcher2) { 44 | log.debug { "in dispatcher 2" } 45 | } 46 | log.debug { "again in dispatcher 1" } 47 | }.asCompletableFuture().join() 48 | } 49 | } 50 | } 51 | 52 | System.setProperty("marker", "dispatchersSwitch-loomoroutines") 53 | Executors.newSingleThreadScheduledExecutor().toDispatcher().use { dispatcher1 -> 54 | Executors.newSingleThreadScheduledExecutor().toDispatcher().use { dispatcher2 -> 55 | doMeasurement { 56 | dispatcher1.dispatch { 57 | log.debug { "in dispatcher 1" } 58 | doIn(dispatcher2) { 59 | log.debug { "in dispatcher 2" } 60 | } 61 | log.debug { "again in dispatcher 1" } 62 | }.join() 63 | } 64 | } 65 | } 66 | } 67 | 68 | @Test 69 | fun dispatcherDelay() { 70 | System.setProperty("marker", "dispatcherDelay-kotlin") 71 | Executors.newSingleThreadScheduledExecutor().asCoroutineDispatcher().use { dispatcher -> 72 | val scope = CoroutineScope(dispatcher) 73 | doMeasurement { 74 | scope.launch { 75 | log.debug { "before delay" } 76 | kotlinDelay(1) 77 | log.debug { "after delay" } 78 | }.asCompletableFuture().join() 79 | } 80 | } 81 | 82 | System.setProperty("marker", "dispatcherDelay-loomoroutines") 83 | Executors.newSingleThreadScheduledExecutor().toDispatcher().use { dispatcher -> 84 | doMeasurement { 85 | dispatcher.dispatch { 86 | log.debug { "before delay" } 87 | delay(1.milliseconds) 88 | log.debug { "after delay" } 89 | }.join() 90 | } 91 | } 92 | } 93 | 94 | @Test 95 | fun dispatcherAwait() { 96 | System.setProperty("marker", "dispatcherAwait-kotlin") 97 | Executors.newSingleThreadScheduledExecutor().asCoroutineDispatcher().use { dispatcher -> 98 | val scope = CoroutineScope(dispatcher) 99 | doMeasurement { 100 | scope.launch { 101 | log.debug { "before await" } 102 | kotlinSuspendCoroutine { 103 | log.debug { "before resume" } 104 | it.resume(null) 105 | log.debug { "after resume" } 106 | } 107 | log.debug { "after await" } 108 | }.asCompletableFuture().join() 109 | } 110 | } 111 | 112 | System.setProperty("marker", "dispatcherAwait-loomoroutines") 113 | Executors.newSingleThreadScheduledExecutor().toDispatcher().use { dispatcher -> 114 | doMeasurement { 115 | dispatcher.dispatch { 116 | log.debug { "before await" } 117 | await { 118 | log.debug { "before resume" } 119 | it() 120 | log.debug { "after resume" } 121 | } 122 | log.debug { "after await" } 123 | }.join() 124 | } 125 | } 126 | } 127 | } 128 | 129 | inline fun generateFibonacci(emit: (Long) -> Unit) { 130 | var prev = 0L 131 | var curr = 1L 132 | while (true) { 133 | emit(curr) 134 | val tmp = curr + prev 135 | prev = curr 136 | curr = tmp 137 | } 138 | } 139 | 140 | inline fun doMeasurement( 141 | repeatTimes: Int = 1000, 142 | logRound: (TimedValue) -> Unit = { log.debug { "got item ${it.value} in ${it.duration}" } }, 143 | logSummary: (average: Duration, median: Duration) -> Unit = { average: Duration, median: Duration -> 144 | log.info { "median duration is $median" } 145 | log.info { "average duration is $average" } 146 | }, 147 | action: () -> T 148 | ) { 149 | val times = mutableListOf() 150 | repeat(repeatTimes) { 151 | val timedValue = measureTimedValue(action) 152 | logRound(timedValue) 153 | times.add(timedValue.duration) 154 | } 155 | times.sort() 156 | logSummary( 157 | times.reduce(Duration::plus) / repeatTimes, 158 | (times[times.size / 2] + times[(times.size - 1) / 2]) / 2 159 | ) 160 | } 161 | -------------------------------------------------------------------------------- /tests-kotlin/src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} [%property{marker}] %msg%n 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /tests-nomodule/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `java-library` 3 | } 4 | 5 | repositories { 6 | mavenCentral() 7 | } 8 | 9 | dependencies { 10 | testImplementation(project(":loomoroutines-common")) 11 | testImplementation(project(":loomoroutines-dispatcher")) 12 | testImplementation("org.junit.jupiter:junit-jupiter-api:${properties["jupiterVersion"]}") 13 | testImplementation("org.apache.commons:commons-lang3:${properties["commonsLang3Version"]}") 14 | testRuntimeOnly("ch.qos.logback:logback-classic:${properties["logbackVersion"]}") 15 | testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${properties["jupiterVersion"]}") 16 | } 17 | 18 | java { 19 | sourceCompatibility = JavaVersion.VERSION_21 20 | targetCompatibility = JavaVersion.VERSION_21 21 | } 22 | 23 | tasks.test { 24 | useJUnitPlatform() 25 | jvmArgs("--add-exports", "java.base/jdk.internal.vm=ALL-UNNAMED") 26 | } 27 | -------------------------------------------------------------------------------- /tests-nomodule/src/test/java/ExampleGenerator.java: -------------------------------------------------------------------------------- 1 | import java.math.BigInteger; 2 | 3 | import static dev.reformator.loomoroutines.common.GeneratorUtils.loomStream; 4 | 5 | public class ExampleGenerator { 6 | public static void main(String[] args) { 7 | var fibinacciStream = loomStream(scope -> { 8 | var previous = BigInteger.ZERO; 9 | var current = BigInteger.ONE; 10 | while (true) { 11 | scope.emit(current); 12 | var tmp = previous.add(current); 13 | previous = current; 14 | current = tmp; 15 | } 16 | }); 17 | fibinacciStream.limit(50).forEach(System.out::println); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests-nomodule/src/test/java/ExampleSwing.java: -------------------------------------------------------------------------------- 1 | import dev.reformator.loomoroutines.dispatcher.SwingDispatcher; 2 | import dev.reformator.loomoroutines.dispatcher.VirtualThreadsDispatcher; 3 | 4 | import javax.imageio.ImageIO; 5 | import javax.swing.*; 6 | 7 | import java.awt.*; 8 | import java.awt.image.BufferedImage; 9 | import java.io.IOException; 10 | import java.net.URI; 11 | import java.time.Duration; 12 | import java.util.regex.Pattern; 13 | 14 | import static dev.reformator.loomoroutines.dispatcher.DispatcherUtils.*; 15 | 16 | public class ExampleSwing { 17 | private static int pickingCatCounter = 0; 18 | 19 | private static final Pattern urlPattern = Pattern.compile("\"url\":\"([^\"]+)\""); 20 | 21 | public static void main(String[] args) { 22 | var frame = new JFrame("Cats"); 23 | frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 24 | var panel = new JPanel(); 25 | panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); 26 | var button = new JButton("Pick a cat"); 27 | var imagePanel = new ImagePanel(); 28 | panel.add(button); 29 | panel.add(imagePanel); 30 | frame.add(panel); 31 | frame.setSize(1000, 500); 32 | frame.setVisible(true); 33 | 34 | button.addActionListener(e -> dispatch(SwingDispatcher.INSTANCE, () -> { 35 | pickingCatCounter++; 36 | if (pickingCatCounter % 2 == 0) { 37 | button.setText("Pick another cat"); 38 | return null; 39 | } else { 40 | button.setText("This one!"); 41 | var cachedPickingCatCounter = pickingCatCounter; 42 | 43 | try { 44 | while (true) { 45 | var bufferedImage = doIn(VirtualThreadsDispatcher.INSTANCE, ExampleSwing::loadCatImage); 46 | if (pickingCatCounter != cachedPickingCatCounter) { 47 | return null; 48 | } 49 | 50 | imagePanel.setImage(bufferedImage); 51 | delay(Duration.ofSeconds(1)); 52 | 53 | if (pickingCatCounter != cachedPickingCatCounter) { 54 | return null; 55 | } 56 | } 57 | } catch (Throwable ex) { 58 | if (pickingCatCounter == cachedPickingCatCounter) { 59 | ex.printStackTrace(); 60 | pickingCatCounter++; 61 | button.setText("Exception: " + ex.getMessage() + ". Try again?"); 62 | } 63 | return null; 64 | } 65 | } 66 | })); 67 | } 68 | 69 | private static BufferedImage loadCatImage() { 70 | String url; 71 | { 72 | String json; 73 | try (var stream = URI.create("https://api.thecatapi.com/v1/images/search").toURL().openStream()) { 74 | json = new String(stream.readAllBytes()); 75 | } catch (IOException ex) { 76 | throw new RuntimeException(ex); 77 | } 78 | var mather = urlPattern.matcher(json); 79 | if (!mather.find()) { 80 | throw new RuntimeException("cat url is not found in json '" + json + "'"); 81 | } 82 | url = mather.group(1); 83 | } 84 | try (var stream = URI.create(url).toURL().openStream()) { 85 | return ImageIO.read(stream); 86 | } catch (IOException ex) { 87 | throw new RuntimeException(ex); 88 | } 89 | } 90 | } 91 | 92 | class ImagePanel extends JPanel { 93 | private BufferedImage image = null; 94 | 95 | public void setImage(BufferedImage image) { 96 | this.image = image; 97 | repaint(); 98 | } 99 | 100 | @Override 101 | protected void paintComponent(Graphics g) { 102 | super.paintComponent(g); 103 | if (image != null) { 104 | g.drawImage(image, 0, 0, null); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /tests-nomodule/src/test/java/NomoduleTest.java: -------------------------------------------------------------------------------- 1 | import dev.reformator.loomoroutines.common.CompletedCoroutine; 2 | import dev.reformator.loomoroutines.common.GeneratorUtils; 3 | import dev.reformator.loomoroutines.common.SuspendedCoroutine; 4 | import org.apache.commons.lang3.mutable.MutableBoolean; 5 | import org.apache.commons.lang3.mutable.MutableInt; 6 | import org.junit.jupiter.api.Assertions; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import java.util.List; 10 | import java.util.NoSuchElementException; 11 | import java.util.Objects; 12 | 13 | import static dev.reformator.loomoroutines.common.CoroutineUtils.*; 14 | import static dev.reformator.loomoroutines.common.GeneratorUtils.*; 15 | import static org.junit.jupiter.api.Assertions.*; 16 | 17 | public class NomoduleTest { 18 | @Test 19 | public void testGenerators() { 20 | var iterator = loomIterator(scope -> { 21 | for (int i = 0; i < 10; i++) { 22 | scope.emit(i); 23 | } 24 | scope.emit(10); 25 | for (var value: loomIterable(innerScope -> { 26 | for (int i = 11; i < 20; i++) { 27 | if (i % 2 == 0) { 28 | innerScope.emit(i); 29 | } else { 30 | scope.emit(i); 31 | } 32 | } 33 | 34 | GeneratorUtils.loomStream((innerInnerScope) -> { 35 | for (int i = 20; i <= 30; i++) { 36 | if (i % 3 == 0) { 37 | innerInnerScope.emit(i); 38 | } else if (i % 3 == 1) { 39 | innerScope.emit(i); 40 | } else { 41 | scope.emit(i); 42 | } 43 | } 44 | }).forEach(i -> { 45 | if (i % 2 == 0) { 46 | innerScope.emit(i); 47 | } else { 48 | scope.emit(i); 49 | } 50 | }); 51 | })) { 52 | scope.emit(value); 53 | } 54 | }); 55 | 56 | for (int i = 0; i<= 30; i++) { 57 | assertTrue(iterator.hasNext()); 58 | assertEquals(i, iterator.next()); 59 | } 60 | assertFalse(iterator.hasNext()); 61 | assertThrows(NoSuchElementException.class, iterator::next); 62 | } 63 | 64 | @Test 65 | public void testCoroutineContextApi() { 66 | var context1CallCounter = new MutableInt(); 67 | createCoroutine("context1", () -> { 68 | context1CallCounter.increment(); 69 | 70 | assertEquals( 71 | List.of("context1"), 72 | getRunningCoroutinesContexts() 73 | ); 74 | assertEquals(1, getRunningCoroutinesNumber()); 75 | 76 | var callCounter = new MutableInt(); 77 | assertEquals("context1", getRunningCoroutineContext(context -> { 78 | callCounter.increment(); 79 | assertEquals("context1", context); 80 | return true; 81 | })); 82 | assertEquals(1, callCounter.intValue()); 83 | 84 | var context2CallCounter = new MutableInt(); 85 | createCoroutine("context2", () -> { 86 | context2CallCounter.increment(); 87 | 88 | assertEquals( 89 | List.of("context2", "context1"), 90 | getRunningCoroutinesContexts() 91 | ); 92 | assertEquals(2, getRunningCoroutinesNumber()); 93 | 94 | callCounter.setValue(0); 95 | assertEquals("context1", getRunningCoroutineContext(context -> { 96 | boolean result; 97 | if (callCounter.intValue() == 0) { 98 | assertEquals("context2", context); 99 | result = false; 100 | } else if (callCounter.intValue() == 1) { 101 | assertEquals("context1", context); 102 | result = true; 103 | } else { 104 | throw Assertions.fail("invalid callCounter: " + callCounter.intValue()); 105 | } 106 | callCounter.increment(); 107 | return result; 108 | })); 109 | assertEquals(2, callCounter.intValue()); 110 | 111 | var context3CallCounter = new MutableInt(); 112 | var coroutine3 = createCoroutine(List.of(120), () -> { 113 | context3CallCounter.increment(); 114 | 115 | callCounter.setValue(0); 116 | assertEquals( 117 | List.of(120), 118 | getRunningCoroutineContext(List.class, list -> { 119 | callCounter.increment(); 120 | assertEquals(120, list.get(0)); 121 | return true; 122 | }) 123 | ); 124 | assertEquals(1, callCounter.intValue()); 125 | 126 | assertEquals(List.of(120), getRunningCoroutineContext(List.class)); 127 | }); 128 | assertEquals(0, context3CallCounter.intValue()); 129 | assertEquals( 130 | List.of(120), 131 | assertInstanceOf(CompletedCoroutine.class, coroutine3.resume()).getCoroutineContext() 132 | ); 133 | assertEquals(1, context3CallCounter.intValue()); 134 | }).resume(); 135 | assertEquals(1, context2CallCounter.intValue()); 136 | }).resume(); 137 | assertEquals(1, context1CallCounter.intValue()); 138 | } 139 | 140 | @Test 141 | public void testCoroutineSuspensionApi() { 142 | var callCounter = new MutableInt(); 143 | var coroutine1 = createCoroutine("context1", () -> { 144 | assertEquals(1, callCounter.getAndIncrement()); 145 | assertFalse(trySuspendCoroutine(context -> { 146 | assertEquals(2, callCounter.getAndIncrement()); 147 | assertEquals("context1", context); 148 | return false; 149 | })); 150 | assertEquals(3, callCounter.getAndIncrement()); 151 | assertTrue(trySuspendCoroutine(context -> { 152 | assertEquals(4, callCounter.getAndIncrement()); 153 | assertEquals("context1", context); 154 | return true; 155 | })); 156 | assertEquals(6, callCounter.getAndIncrement()); 157 | }); 158 | assertEquals("context1", coroutine1.getCoroutineContext()); 159 | assertEquals(0, callCounter.getAndIncrement()); 160 | var coroutine2 = assertInstanceOf(SuspendedCoroutine.class, coroutine1.resume()); 161 | assertEquals("context1", coroutine2.getCoroutineContext()); 162 | assertEquals(5, callCounter.getAndIncrement()); 163 | var coroutine3 = assertInstanceOf(CompletedCoroutine.class, coroutine2.resume()); 164 | assertEquals(7, callCounter.getAndIncrement()); 165 | assertEquals("context1", coroutine3.getCoroutineContext()); 166 | } 167 | 168 | @Test 169 | public void testPinnedVirtualThread() throws InterruptedException { 170 | var completed =new MutableBoolean(false); 171 | Thread.startVirtualThread(() -> { 172 | synchronized (new Object()) { 173 | createCoroutine("context1", () -> { 174 | try { 175 | Thread.sleep(100); 176 | } catch (InterruptedException e) { 177 | throw new RuntimeException(e); 178 | } 179 | }).resume(); 180 | } 181 | completed.setTrue(); 182 | }).join(); 183 | assertTrue(completed.getValue()); 184 | } 185 | 186 | @Test 187 | public void testFailedSuspend() { 188 | try { 189 | createCoroutine("context1", () -> { 190 | synchronized (new Object()) { 191 | createCoroutine("context2", () -> { 192 | suspendCoroutine(c -> Objects.equals(c, "context1")); 193 | }).resume(); 194 | } 195 | }).resume(); 196 | } catch (IllegalStateException e) { 197 | assertTrue(e.getMessage().contains("Current thread is pinned")); 198 | return; 199 | } 200 | fail(); 201 | } 202 | 203 | @Test 204 | public void testNotFailedSuspend() { 205 | createCoroutine("context1", () -> { 206 | synchronized (new Object()) { 207 | createCoroutine("context2", () -> { 208 | suspendCoroutine(c -> Objects.equals(c, "context2")); 209 | }).resume(); 210 | } 211 | }).resume(); 212 | } 213 | } 214 | --------------------------------------------------------------------------------