diff --git a/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/zipline/ZiplineDispatcher.kt b/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/zipline/ZiplineDispatcher.kt index 8ee39b8f..99496217 100644 --- a/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/zipline/ZiplineDispatcher.kt +++ b/composeApp/src/commonMain/kotlin/org/spotube/spotube/core/zipline/ZiplineDispatcher.kt @@ -40,7 +40,8 @@ class ZiplineDispatcher( * size is ~1 MiB, which causes native stack overflow when compiling large * JS modules like kotlin-stdlib (~491 KB). * - * This expect/actual ensures the backing thread has a sufficiently large stack. + * iOS secondary threads get only 512 KiB, so every platform must create its own + * large-stack thread. This expect/actual ensures the backing thread has one. */ expect fun createZiplineDispatcher(): ZiplineDispatcher diff --git a/composeApp/src/iosMain/kotlin/org/spotube/spotube/core/zipline/ZiplineDispatcher.ios.kt b/composeApp/src/iosMain/kotlin/org/spotube/spotube/core/zipline/ZiplineDispatcher.ios.kt index 8249ae1e..c4a1559a 100644 --- a/composeApp/src/iosMain/kotlin/org/spotube/spotube/core/zipline/ZiplineDispatcher.ios.kt +++ b/composeApp/src/iosMain/kotlin/org/spotube/spotube/core/zipline/ZiplineDispatcher.ios.kt @@ -17,17 +17,32 @@ package org.spotube.spotube.core.zipline -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.newSingleThreadContext +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.runBlocking +import platform.Foundation.NSThread +import kotlin.coroutines.ContinuationInterceptor + +// Same as JVM/Android. Zipline lets QuickJS recurse up to ~6 MiB of native stack, but +// iOS gives secondary threads (incl. newSingleThreadContext's) only 512 KiB, so loading +// a plugin overflowed into the guard page and crashed the app with SIGBUS. +private const val ZIPLINE_THREAD_STACK_SIZE: ULong = 8_388_608u // 8 MiB -@OptIn(ExperimentalCoroutinesApi::class, DelicateCoroutinesApi::class) actual fun createZiplineDispatcher(): ZiplineDispatcher { - // On iOS/native, thread stack sizes are typically 8 MiB by default, - // so newSingleThreadContext is sufficient. - val ctx = newSingleThreadContext("Zipline") - return ZiplineDispatcher(ctx) { - ctx.close() + val dispatcher = CompletableDeferred() + val stopped = CompletableDeferred() + // NSThread is the only way to choose a thread's stack size here; it hosts an event + // loop (runBlocking) whose dispatcher accepts work from any thread. + val thread = NSThread { + runBlocking { + dispatcher.complete(coroutineContext[ContinuationInterceptor] as CoroutineDispatcher) + stopped.await() + } + } + thread.name = "Zipline" + thread.stackSize = ZIPLINE_THREAD_STACK_SIZE + thread.start() + return ZiplineDispatcher(runBlocking { dispatcher.await() }) { + stopped.complete(Unit) } } -