Skip to content

Commit 4be5899

Browse files
authored
Add the Wasm/WASI target support (#4064)
1 parent 61dd23c commit 4be5899

File tree

30 files changed

+436
-91
lines changed

30 files changed

+436
-91
lines changed

buildSrc/src/main/kotlin/AuxBuildConfiguration.kt

-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ object AuxBuildConfiguration {
1919
}
2020

2121
CacheRedirector.configureJsPackageManagers(rootProject)
22-
CacheRedirector.configureWasmNodeRepositories(rootProject)
2322

2423
// Sigh, there is no BuildScanExtension in classpath when there is no --scan
2524
rootProject.extensions.findByName("buildScan")?.withGroovyBuilder {

buildSrc/src/main/kotlin/CacheRedirector.kt

-17
Original file line numberDiff line numberDiff line change
@@ -138,23 +138,6 @@ object CacheRedirector {
138138
project.configureYarnAndNodeRedirects()
139139
}
140140

141-
/**
142-
* Temporary repositories to depend on until GC milestone 4 in KGP
143-
* and stable Node release. Safe to remove when its removal does not break WASM tests.
144-
*/
145-
@JvmStatic
146-
fun configureWasmNodeRepositories(project: Project) {
147-
val extension = project.extensions.findByType<NodeJsRootExtension>()
148-
if (extension != null) {
149-
extension.nodeVersion = "21.0.0-v8-canary202309167e82ab1fa2"
150-
extension.nodeDownloadBaseUrl = "https://nodejs.org/download/v8-canary"
151-
}
152-
153-
project.tasks.withType<KotlinNpmInstallTask>().configureEach {
154-
args.add("--ignore-engines")
155-
}
156-
}
157-
158141
@JvmStatic
159142
fun maybeRedirect(url: String): String {
160143
if (!cacheRedirectorEnabled) return url

buildSrc/src/main/kotlin/kotlin-multiplatform-conventions.gradle.kts

+25-2
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,18 @@ kotlin {
6363
api("org.jetbrains.kotlinx:atomicfu-wasm-js:${version("atomicfu")}")
6464
}
6565
}
66+
@OptIn(org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl::class)
67+
wasmWasi {
68+
nodejs()
69+
compilations["main"]?.dependencies {
70+
api("org.jetbrains.kotlinx:atomicfu-wasm-wasi:${version("atomicfu")}")
71+
}
72+
compilations.configureEach {
73+
compilerOptions.configure {
74+
optIn.add("kotlin.wasm.internal.InternalWasmApi")
75+
}
76+
}
77+
}
6678
applyDefaultHierarchyTemplate()
6779
sourceSets {
6880
commonTest {
@@ -101,15 +113,26 @@ kotlin {
101113
api("org.jetbrains.kotlin:kotlin-test-wasm-js:${version("kotlin")}")
102114
}
103115
}
104-
groupSourceSets("jsAndWasmShared", listOf("js", "wasmJs"), listOf("common"))
116+
val wasmWasiMain by getting {
117+
}
118+
val wasmWasiTest by getting {
119+
dependencies {
120+
api("org.jetbrains.kotlin:kotlin-test-wasm-wasi:${version("kotlin")}")
121+
}
122+
}
123+
groupSourceSets("jsAndWasmJsShared", listOf("js", "wasmJs"), emptyList())
124+
groupSourceSets("jsAndWasmShared", listOf("jsAndWasmJsShared", "wasmWasi"), listOf("common"))
105125
}
106126
}
107127

108-
// Disable intermediate sourceSet compilation because we do not need js-wasmJs artifact
128+
// Disable intermediate sourceSet compilation because we do not need js-wasm common artifact
109129
tasks.configureEach {
110130
if (name == "compileJsAndWasmSharedMainKotlinMetadata") {
111131
enabled = false
112132
}
133+
if (name == "compileJsAndWasmJsSharedMainKotlinMetadata") {
134+
enabled = false
135+
}
113136
}
114137

115138
tasks.named("jvmTest", Test::class) {

integration-testing/smokeTest/build.gradle

+5
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ kotlin {
4848
implementation kotlin('test-wasm-js')
4949
}
5050
}
51+
wasmWasiTest {
52+
dependencies {
53+
implementation kotlin('test-wasm-wasi')
54+
}
55+
}
5156
jvmTest {
5257
dependencies {
5358
implementation kotlin('test')

kotlinx-coroutines-core/api/kotlinx-coroutines-core.klib.api

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Klib ABI Dump
2-
// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, iosArm64, iosSimulatorArm64, iosX64, js, linuxArm64, linuxX64, macosArm64, macosX64, mingwX64, tvosArm64, tvosSimulatorArm64, tvosX64, wasmJs, watchosArm32, watchosArm64, watchosDeviceArm64, watchosSimulatorArm64, watchosX64]
2+
// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, iosArm64, iosSimulatorArm64, iosX64, js, linuxArm64, linuxX64, macosArm64, macosX64, mingwX64, tvosArm64, tvosSimulatorArm64, tvosX64, wasmJs, wasmWasi, watchosArm32, watchosArm64, watchosDeviceArm64, watchosSimulatorArm64, watchosX64]
33
// Alias: native => [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, iosArm64, iosSimulatorArm64, iosX64, linuxArm64, linuxX64, macosArm64, macosX64, mingwX64, tvosArm64, tvosSimulatorArm64, tvosX64, watchosArm32, watchosArm64, watchosDeviceArm64, watchosSimulatorArm64, watchosX64]
44
// Rendering settings:
55
// - Signature version: 2

kotlinx-coroutines-core/build.gradle.kts

+7-4
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,13 @@ apply(plugin = "pub-conventions")
1818
Configure source sets structure for kotlinx-coroutines-core:
1919
2020
TARGETS SOURCE SETS
21-
------- ----------------------------------------------
22-
wasmJs \----------> jsAndWasmShared --------------------+
23-
js / |
24-
V
21+
------------------------------------------------------------
22+
wasmJs \------> jsAndWasmJsShared ----+
23+
js / |
24+
V
25+
wasmWasi --------------------> jsAndWasmShared ----------+
26+
|
27+
V
2528
jvmCore\ --------> jvm ---------> concurrent -------> common
2629
jdk8 / ^
2730
|

kotlinx-coroutines-core/common/test/flow/VirtualTime.kt

+15-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,21 @@ internal class VirtualTimeDispatcher(enclosingScope: CoroutineScope) : Coroutine
2222
val delayNanos = ThreadLocalEventLoop.currentOrNull()?.processNextEvent()
2323
?: error("Event loop is missing, virtual time source works only as part of event loop")
2424
if (delayNanos <= 0) continue
25-
if (delayNanos > 0 && delayNanos != Long.MAX_VALUE) error("Unexpected external delay: $delayNanos")
25+
if (delayNanos > 0 && delayNanos != Long.MAX_VALUE) {
26+
if (usesSharedEventLoop) {
27+
val targetTime = currentTime + delayNanos
28+
while (currentTime < targetTime) {
29+
val nextTask = heap.minByOrNull { it.deadline } ?: break
30+
if (nextTask.deadline > targetTime) break
31+
heap.remove(nextTask)
32+
currentTime = nextTask.deadline
33+
nextTask.run()
34+
}
35+
currentTime = maxOf(currentTime, targetTime)
36+
} else {
37+
error("Unexpected external delay: $delayNanos")
38+
}
39+
}
2640
val nextTask = heap.minByOrNull { it.deadline } ?: return@launch
2741
heap.remove(nextTask)
2842
currentTime = nextTask.deadline
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package kotlinx.coroutines
22

33
import kotlinx.browser.*
4-
import kotlinx.coroutines.internal.*
5-
import kotlin.coroutines.*
64

75
private external val navigator: dynamic
86
private const val UNDEFINED = "undefined"
@@ -28,30 +26,3 @@ private fun isJsdom() = jsTypeOf(navigator) != UNDEFINED &&
2826
jsTypeOf(navigator.userAgent) != UNDEFINED &&
2927
jsTypeOf(navigator.userAgent.match) != UNDEFINED &&
3028
navigator.userAgent.match("\\bjsdom\\b")
31-
32-
@PublishedApi // Used from kotlinx-coroutines-test via suppress, not part of ABI
33-
internal actual val DefaultDelay: Delay
34-
get() = Dispatchers.Default as Delay
35-
36-
public actual fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext {
37-
val combined = coroutineContext + context
38-
return if (combined !== Dispatchers.Default && combined[ContinuationInterceptor] == null)
39-
combined + Dispatchers.Default else combined
40-
}
41-
42-
public actual fun CoroutineContext.newCoroutineContext(addedContext: CoroutineContext): CoroutineContext {
43-
return this + addedContext
44-
}
45-
46-
// No debugging facilities on JS
47-
internal actual inline fun <T> withCoroutineContext(context: CoroutineContext, countOrElement: Any?, block: () -> T): T = block()
48-
internal actual inline fun <T> withContinuationContext(continuation: Continuation<*>, countOrElement: Any?, block: () -> T): T = block()
49-
internal actual fun Continuation<*>.toDebugString(): String = toString()
50-
internal actual val CoroutineContext.coroutineName: String? get() = null // not supported on JS
51-
52-
internal actual class UndispatchedCoroutine<in T> actual constructor(
53-
context: CoroutineContext,
54-
uCont: Continuation<T>
55-
) : ScopeCoroutine<T>(context, uCont) {
56-
override fun afterResume(state: Any?) = uCont.resumeWith(recoverResult(state, uCont))
57-
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package kotlinx.coroutines
2+
3+
import kotlinx.coroutines.internal.ScopeCoroutine
4+
import kotlin.coroutines.*
5+
6+
@PublishedApi // Used from kotlinx-coroutines-test via suppress, not part of ABI
7+
internal actual val DefaultDelay: Delay
8+
get() = Dispatchers.Default as Delay
9+
10+
public actual fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext {
11+
val combined = coroutineContext + context
12+
return if (combined !== Dispatchers.Default && combined[ContinuationInterceptor] == null)
13+
combined + Dispatchers.Default else combined
14+
}
15+
16+
public actual fun CoroutineContext.newCoroutineContext(addedContext: CoroutineContext): CoroutineContext {
17+
return this + addedContext
18+
}
19+
20+
// No debugging facilities on Wasm and JS
21+
internal actual inline fun <T> withCoroutineContext(context: CoroutineContext, countOrElement: Any?, block: () -> T): T = block()
22+
internal actual inline fun <T> withContinuationContext(continuation: Continuation<*>, countOrElement: Any?, block: () -> T): T = block()
23+
internal actual fun Continuation<*>.toDebugString(): String = toString()
24+
internal actual val CoroutineContext.coroutineName: String? get() = null // not supported on Wasm and JS
25+
26+
internal actual class UndispatchedCoroutine<in T> actual constructor(
27+
context: CoroutineContext,
28+
uCont: Continuation<T>
29+
) : ScopeCoroutine<T>(context, uCont) {
30+
override fun afterResume(state: Any?) = uCont.resumeWith(recoverResult(state, uCont))
31+
}
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package kotlinx.coroutines
22

3-
import kotlinx.coroutines.internal.*
43
import org.w3c.dom.*
5-
import kotlin.coroutines.*
64

75
internal external interface JsProcess : JsAny {
86
fun nextTick(handler: () -> Unit)
@@ -18,30 +16,3 @@ internal actual fun createDefaultDispatcher(): CoroutineDispatcher =
1816
tryGetProcess()?.let(::NodeDispatcher)
1917
?: tryGetWindow()?.let(::WindowDispatcher)
2018
?: SetTimeoutDispatcher
21-
22-
@PublishedApi // Used from kotlinx-coroutines-test via suppress, not part of ABI
23-
internal actual val DefaultDelay: Delay
24-
get() = Dispatchers.Default as Delay
25-
26-
public actual fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext {
27-
val combined = coroutineContext + context
28-
return if (combined !== Dispatchers.Default && combined[ContinuationInterceptor] == null)
29-
combined + Dispatchers.Default else combined
30-
}
31-
32-
public actual fun CoroutineContext.newCoroutineContext(addedContext: CoroutineContext): CoroutineContext {
33-
return this + addedContext
34-
}
35-
36-
// No debugging facilities on Wasm
37-
internal actual inline fun <T> withCoroutineContext(context: CoroutineContext, countOrElement: Any?, block: () -> T): T = block()
38-
internal actual inline fun <T> withContinuationContext(continuation: Continuation<*>, countOrElement: Any?, block: () -> T): T = block()
39-
internal actual fun Continuation<*>.toDebugString(): String = toString()
40-
internal actual val CoroutineContext.coroutineName: String? get() = null // not supported on Wasm
41-
42-
internal actual class UndispatchedCoroutine<in T> actual constructor(
43-
context: CoroutineContext,
44-
uCont: Continuation<T>
45-
) : ScopeCoroutine<T>(context, uCont) {
46-
override fun afterResume(state: Any?) = uCont.resumeWith(recoverResult(state, uCont))
47-
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package kotlinx.coroutines
2+
3+
internal actual val DEBUG: Boolean = false
4+
5+
internal actual val Any.hexAddress: String
6+
get() = this.hashCode().toString()
7+
8+
internal actual val Any.classSimpleName: String get() = this::class.simpleName ?: "Unknown"
9+
10+
internal actual inline fun assert(value: () -> Boolean) {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
@file:OptIn(UnsafeWasmMemoryApi::class)
2+
package kotlinx.coroutines
3+
4+
import kotlin.coroutines.CoroutineContext
5+
import kotlin.wasm.*
6+
import kotlin.wasm.unsafe.*
7+
8+
@WasmImport("wasi_snapshot_preview1", "poll_oneoff")
9+
private external fun wasiPollOneOff(ptrToSubscription: Int, eventPtr: Int, nsubscriptions: Int, resultPtr: Int): Int
10+
11+
@WasmImport("wasi_snapshot_preview1", "clock_time_get")
12+
private external fun wasiRawClockTimeGet(clockId: Int, precision: Long, resultPtr: Int): Int
13+
14+
private const val CLOCKID_MONOTONIC = 1
15+
16+
internal actual fun createEventLoop(): EventLoop = DefaultExecutor
17+
18+
internal actual fun nanoTime(): Long = withScopedMemoryAllocator { allocator: MemoryAllocator ->
19+
val ptrTo8Bytes = allocator.allocate(8)
20+
val returnCode = wasiRawClockTimeGet(
21+
clockId = CLOCKID_MONOTONIC,
22+
precision = 1,
23+
resultPtr = ptrTo8Bytes.address.toInt()
24+
)
25+
check(returnCode == 0) { "clock_time_get failed with the return code $returnCode" }
26+
ptrTo8Bytes.loadLong()
27+
}
28+
29+
private fun sleep(nanos: Long, ptrTo32Bytes: Pointer, ptrTo8Bytes: Pointer, ptrToSubscription: Pointer) {
30+
//__wasi_timestamp_t timeout;
31+
(ptrToSubscription + 24).storeLong(nanos)
32+
val returnCode = wasiPollOneOff(
33+
ptrToSubscription = ptrToSubscription.address.toInt(),
34+
eventPtr = ptrTo32Bytes.address.toInt(),
35+
nsubscriptions = 1,
36+
resultPtr = ptrTo8Bytes.address.toInt()
37+
)
38+
check(returnCode == 0) { "poll_oneoff failed with the return code $returnCode" }
39+
}
40+
41+
internal actual object DefaultExecutor : EventLoopImplBase() {
42+
override fun shutdown() {
43+
// don't do anything: on WASI, the event loop is the default executor, we can't shut it down
44+
}
45+
46+
override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle =
47+
scheduleInvokeOnTimeout(timeMillis, block)
48+
49+
actual override fun enqueue(task: Runnable) {
50+
if (kotlin.wasm.internal.onExportedFunctionExit == null) {
51+
kotlin.wasm.internal.onExportedFunctionExit = ::runEventLoop
52+
}
53+
super.enqueue(task)
54+
}
55+
}
56+
57+
internal actual abstract class EventLoopImplPlatform : EventLoop() {
58+
protected actual fun unpark() {
59+
// do nothing: in WASI, no external callbacks can be invoked while `poll_oneoff` is running,
60+
// so it is both impossible and unnecessary to unpark the event loop
61+
}
62+
63+
protected actual fun reschedule(now: Long, delayedTask: EventLoopImplBase.DelayedTask) {
64+
// throw; on WASI, the event loop is the default executor, we can't shut it down or reschedule tasks
65+
// to anyone else
66+
throw UnsupportedOperationException("runBlocking event loop is not supported")
67+
}
68+
}
69+
70+
internal actual inline fun platformAutoreleasePool(crossinline block: () -> Unit) = block()
71+
72+
internal fun runEventLoop() {
73+
withScopedMemoryAllocator { allocator ->
74+
val ptrToSubscription = initializeSubscriptionPtr(allocator)
75+
val ptrTo32Bytes = allocator.allocate(32)
76+
val ptrTo8Bytes = allocator.allocate(8)
77+
val eventLoop = DefaultExecutor
78+
eventLoop.incrementUseCount()
79+
try {
80+
while (true) {
81+
val parkNanos = eventLoop.processNextEvent()
82+
if (parkNanos == Long.MAX_VALUE) {
83+
// no more events
84+
break
85+
}
86+
if (parkNanos > 0) {
87+
// sleep until the next event
88+
sleep(
89+
parkNanos,
90+
ptrTo32Bytes = ptrTo32Bytes,
91+
ptrTo8Bytes = ptrTo8Bytes,
92+
ptrToSubscription = ptrToSubscription
93+
)
94+
}
95+
}
96+
} finally { // paranoia
97+
eventLoop.decrementUseCount()
98+
}
99+
}
100+
}
101+
102+
private fun initializeSubscriptionPtr(allocator: MemoryAllocator): Pointer {
103+
val ptrToSubscription = allocator.allocate(48)
104+
//userdata
105+
ptrToSubscription.storeLong(0)
106+
//uint8_t tag;
107+
(ptrToSubscription + 8).storeByte(0) //EVENTTYPE_CLOCK
108+
//__wasi_clockid_t id;
109+
(ptrToSubscription + 16).storeInt(CLOCKID_MONOTONIC) //CLOCKID_MONOTONIC
110+
//__wasi_timestamp_t timeout;
111+
//(ptrToSubscription + 24).storeLong(timeout)
112+
//__wasi_timestamp_t precision;
113+
(ptrToSubscription + 32).storeLong(0)
114+
//__wasi_subclockflags_t
115+
(ptrToSubscription + 40).storeShort(0) //ABSOLUTE_TIME=1/RELATIVE=0
116+
117+
return ptrToSubscription
118+
}
119+
120+
internal actual fun createDefaultDispatcher(): CoroutineDispatcher = DefaultExecutor

0 commit comments

Comments
 (0)