fix(ios): run Zipline on an 8 MiB stack thread

newSingleThreadContext threads get only 512 KiB of stack on iOS, while
QuickJS may recurse ~6 MiB deep when compiling a plugin, so the app crashed
with SIGBUS at startup once a plugin was installed. Host the dispatcher on an
NSThread with an 8 MiB stack, matching JVM/Android.
This commit is contained in:
Kingkor Roy Tirtho 2026-10-23 11:54:29 +06:00
parent ff7a1714ff
commit 7eb56c8593
2 changed files with 27 additions and 11 deletions

View File

@ -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

View File

@ -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<CoroutineDispatcher>()
val stopped = CompletableDeferred<Unit>()
// 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)
}
}