From ab4da3fd36719859e6a5b4e16a1f490f58ef49fc Mon Sep 17 00:00:00 2001 From: Daniel Lin Date: Fri, 6 Dec 2024 11:41:42 -0500 Subject: [PATCH 1/2] Figured out better node/wasmjs interop --- .../kotlin/com/github/ephemient/aoc2024/exe/WasmJsMain.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/kt/aoc2024-exe/src/wasmJsMain/kotlin/com/github/ephemient/aoc2024/exe/WasmJsMain.kt b/kt/aoc2024-exe/src/wasmJsMain/kotlin/com/github/ephemient/aoc2024/exe/WasmJsMain.kt index 39b734e..f02523d 100644 --- a/kt/aoc2024-exe/src/wasmJsMain/kotlin/com/github/ephemient/aoc2024/exe/WasmJsMain.kt +++ b/kt/aoc2024-exe/src/wasmJsMain/kotlin/com/github/ephemient/aoc2024/exe/WasmJsMain.kt @@ -1,7 +1,8 @@ package com.github.ephemient.aoc2024.exe -private fun argv(): String = js("process.argv.join(' ')") +private fun argv(): JsArray = js("process.argv") suspend fun main() { - mainImpl(argv().split(' ').drop(2).toTypedArray()) + val argv = argv() + mainImpl(Array(argv.length - 2) { argv[it + 2].toString() }) } From 122bde9e77aed9d4087879468d7d8383bb9695f4 Mon Sep 17 00:00:00 2001 From: Daniel Lin Date: Fri, 6 Dec 2024 17:55:53 -0500 Subject: [PATCH 2/2] Enable WASM WASI --- .github/workflows/kt.yml | 29 +++- kt/README.md | 2 +- kt/aoc2024-exe/build.gradle.kts | 30 +++- .../com/github/ephemient/aoc2024/exe/IO.kt | 3 + .../github/ephemient/aoc2024/exe/WasiArgv.kt | 39 +++++ .../ephemient/aoc2024/exe/WasiFilesystem.kt | 134 ++++++++++++++++++ .../ephemient/aoc2024/exe/WasmWasiMain.kt | 6 + 7 files changed, 236 insertions(+), 7 deletions(-) create mode 100644 kt/aoc2024-exe/src/wasmWasiMain/kotlin/com/github/ephemient/aoc2024/exe/IO.kt create mode 100644 kt/aoc2024-exe/src/wasmWasiMain/kotlin/com/github/ephemient/aoc2024/exe/WasiArgv.kt create mode 100644 kt/aoc2024-exe/src/wasmWasiMain/kotlin/com/github/ephemient/aoc2024/exe/WasiFilesystem.kt create mode 100644 kt/aoc2024-exe/src/wasmWasiMain/kotlin/com/github/ephemient/aoc2024/exe/WasmWasiMain.kt diff --git a/.github/workflows/kt.yml b/.github/workflows/kt.yml index 9e493c2..57217ca 100644 --- a/.github/workflows/kt.yml +++ b/.github/workflows/kt.yml @@ -42,6 +42,10 @@ jobs: with: name: aoc2024-wasmJs path: kt/build/js/packages/aoc2024-aoc2024-exe-wasm-js/kotlin/* + - uses: actions/upload-artifact@v4 + with: + name: aoc2024-wasmWasi + path: kt/aoc2024-exe/build/compileSync/wasmWasi/main/productionExecutable/optimized/* - uses: actions/upload-artifact@v4 with: name: aoc2024-js @@ -61,7 +65,7 @@ jobs: working-directory: kt - uses: actions/upload-artifact@v4 with: - name: aoc2024-native + name: aoc2024-graalvm path: kt/graalvm/build/native/nativeCompile/* run-jvm: @@ -96,7 +100,7 @@ jobs: path: inputs - uses: actions/download-artifact@v4 with: - name: aoc2024-native + name: aoc2024-graalvm - run: chmod +x aoc2024-native - run: ./aoc2024-native env: @@ -138,7 +142,26 @@ jobs: env: AOC2024_DATADIR: inputs - run-node: + run-wasmWasi: + needs: [ get-inputs, build ] + runs-on: ubuntu-latest + + steps: + - uses: actions/download-artifact@v4 + with: + name: inputs + path: inputs + - uses: actions/download-artifact@v4 + with: + name: aoc2024-wasmWasi + - uses: actions/setup-node@v4 + with: + node-version: 22.0.0 + - run: node aoc2024-aoc2024-exe-wasm-wasi.mjs + env: + AOC2024_DATADIR: inputs + + run-js: needs: [ get-inputs, build ] runs-on: ubuntu-latest diff --git a/kt/README.md b/kt/README.md index e80e959..c9b7b68 100644 --- a/kt/README.md +++ b/kt/README.md @@ -18,7 +18,7 @@ Run [kotlinx.benchmark](https://github.com/Kotlin/kotlinx-benchmark) ([JMH](http Print solutions for the inputs provided in local data files: ```sh -./gradlew :aoc2024-exe:jvmRun :aoc2024-exe:runReleaseExecutable{LinuxX64,Macos{X64,Arm64}} :aoc2024-exe:{js,wasmJs}NodeProductionRun +./gradlew :aoc2024-exe:jvmRun :aoc2024-exe:runReleaseExecutable{LinuxX64,Macos{X64,Arm64}} :aoc2024-exe:{js,wasmJs,wasmWasi}NodeProductionRun ``` Run all checks, including [Detekt](https://detekt.github.io/) static code analysis: diff --git a/kt/aoc2024-exe/build.gradle.kts b/kt/aoc2024-exe/build.gradle.kts index 811c162..0b04265 100644 --- a/kt/aoc2024-exe/build.gradle.kts +++ b/kt/aoc2024-exe/build.gradle.kts @@ -8,6 +8,18 @@ plugins { distribution } +class UpdateWasmWrapper(private val mainFile: Provider) : Action { + override fun execute(target: Task) { + val mainFile = mainFile.get().asFile + mainFile.writeText( + mainFile.readText().replace( + " argv, env, ", + " argv, env, preopens: { '/data': env['AOC2024_DATADIR'] ?? '.' }, " + ) + ) + } +} + kotlin { @Suppress("SpreadOperator") listOf( @@ -16,9 +28,21 @@ kotlin { mainClass = "com.github.ephemient.aoc2024.exe.Main" } }, - *arrayOf(wasmJs(), js()).onEach { - it.nodejs() - it.binaries.executable() + wasmJs { + nodejs() + binaries.executable() + }, + wasmWasi { + nodejs() + for (binary in binaries.executable()) { + binary.linkTask.configure { + doLast(UpdateWasmWrapper(binary.mainFile)) + } + } + }, + js { + nodejs() + binaries.executable() }, *arrayOf(linuxArm64(), linuxX64(), macosArm64(), macosX64(), mingwX64()).onEach { it.binaries.executable { diff --git a/kt/aoc2024-exe/src/wasmWasiMain/kotlin/com/github/ephemient/aoc2024/exe/IO.kt b/kt/aoc2024-exe/src/wasmWasiMain/kotlin/com/github/ephemient/aoc2024/exe/IO.kt new file mode 100644 index 0000000..1255193 --- /dev/null +++ b/kt/aoc2024-exe/src/wasmWasiMain/kotlin/com/github/ephemient/aoc2024/exe/IO.kt @@ -0,0 +1,3 @@ +package com.github.ephemient.aoc2024.exe + +internal actual fun getDayInput(day: Int): String = readFile("/data/day$day.txt").decodeToString() diff --git a/kt/aoc2024-exe/src/wasmWasiMain/kotlin/com/github/ephemient/aoc2024/exe/WasiArgv.kt b/kt/aoc2024-exe/src/wasmWasiMain/kotlin/com/github/ephemient/aoc2024/exe/WasiArgv.kt new file mode 100644 index 0000000..ecbc36f --- /dev/null +++ b/kt/aoc2024-exe/src/wasmWasiMain/kotlin/com/github/ephemient/aoc2024/exe/WasiArgv.kt @@ -0,0 +1,39 @@ +package com.github.ephemient.aoc2024.exe + +import kotlin.wasm.unsafe.Pointer +import kotlin.wasm.unsafe.UnsafeWasmMemoryApi +import kotlin.wasm.unsafe.withScopedMemoryAllocator + +@WasmImport("wasi_snapshot_preview1", "args_sizes_get") +private external fun argsSizesGet(argcPtr: UInt, bufsizPtr: UInt): Int + +@WasmImport("wasi_snapshot_preview1", "args_get") +private external fun argsGet(argvPtr: UInt, argvBufPtr: UInt): Int + +@OptIn(UnsafeWasmMemoryApi::class) +internal fun argv(): Array { + val argc: Int + val bufsiz: Int + withScopedMemoryAllocator { allocator -> + val argcPtr = allocator.allocate(Int.SIZE_BYTES) + val bufsizPtr = allocator.allocate(Int.SIZE_BYTES) + val errno = argsSizesGet(argcPtr.address, bufsizPtr.address) + check(errno == 0) { "args_sizes_get: $errno" } + argc = argcPtr.loadInt() + bufsiz = bufsizPtr.loadInt() + } + val buffer = ByteArray(bufsiz) + return withScopedMemoryAllocator { allocator -> + val argvPtr = allocator.allocate(argc * Int.SIZE_BYTES) + val errno = argsGet(argvPtr.address, allocator.allocate(bufsiz).address) + check(errno == 0) { "args_get: $errno" } + Array(argc) { + val argPtr = Pointer(argvPtr.plus(it * Int.SIZE_BYTES).loadInt().toUInt()) + for (i in buffer.indices) { + buffer[i] = argPtr.plus(i).loadByte() + if (buffer[i] == 0.toByte()) return@Array buffer.decodeToString(endIndex = i) + } + error("missing \\0") + } + } +} diff --git a/kt/aoc2024-exe/src/wasmWasiMain/kotlin/com/github/ephemient/aoc2024/exe/WasiFilesystem.kt b/kt/aoc2024-exe/src/wasmWasiMain/kotlin/com/github/ephemient/aoc2024/exe/WasiFilesystem.kt new file mode 100644 index 0000000..2dcb69d --- /dev/null +++ b/kt/aoc2024-exe/src/wasmWasiMain/kotlin/com/github/ephemient/aoc2024/exe/WasiFilesystem.kt @@ -0,0 +1,134 @@ +package com.github.ephemient.aoc2024.exe + +import kotlin.wasm.unsafe.UnsafeWasmMemoryApi +import kotlin.wasm.unsafe.withScopedMemoryAllocator + +@WasmImport("wasi_snapshot_preview1", "fd_prestat_get") +private external fun fdPrestatGet(fd: Int, prestat: UInt): Int + +@WasmImport("wasi_snapshot_preview1", "fd_prestat_dir_name") +private external fun fdPrestatDirName(fd: Int, path: UInt, pathLen: Int): Int + +@OptIn(UnsafeWasmMemoryApi::class) +private val preloadedFds = buildMap { + withScopedMemoryAllocator { allocator -> + val prestatPtr = allocator.allocate(8) + + var fd = 3 + while (true) { + when (val errno = fdPrestatGet(fd, prestatPtr.address)) { + 8 -> break // errno.badf + 0 -> {} // errno.success + else -> error("fd_prestat_get: $errno") + } + when (val prestatTag = prestatPtr.loadByte()) { + 0.toByte() -> { // preopentype.dir + val prNameLen = prestatPtr.plus(Int.SIZE_BYTES).loadInt() + val dirName = withScopedMemoryAllocator { allocator -> + val pathPtr = allocator.allocate(prNameLen) + val errno = fdPrestatDirName(fd, pathPtr.address, prNameLen) + check(errno == 0) { "fd_prestat_dir_name: $errno" } + ByteArray(prNameLen) { pathPtr.plus(it).loadByte() } + }.decodeToString() + put(dirName.removeSuffix("/") + "/", fd) + } + else -> error("unknown propentype $prestatTag") + } + fd++ + } + } +} + +@WasmImport("wasi_snapshot_preview1", "path_open") +private external fun pathOpen( + fd: Int, + dirflags: Int, + path: UInt, + pathLen: Int, + oflags: Short, + fsRightsBase: Long, + fsRightsInheriting: Long, + fdflags: Short, + fdPtr: UInt, +): Int + +@WasmImport("wasi_snapshot_preview1", "fd_filestat_get") +private external fun fdFilestatGet(fd: Int, filestat: UInt): Int + +@WasmImport("wasi_snapshot_preview1", "fd_seek") +private external fun fdSeek(fd: Int, offset: Long, whence: Byte, filesize: UInt): Int + +@WasmImport("wasi_snapshot_preview1", "fd_pread") +private external fun fdPread(fd: Int, iovsPtr: UInt, iovs: Int, offset: Long, sizePtr: UInt): Int + +@WasmImport("wasi_snapshot_preview1", "fd_close") +private external fun fdClose(fd: Int): Int + +// fd_read + fd_seek + fd_filestat_get +private const val READ_RIGHTS = 0x200006L +private const val BUFSIZ = 4096 + +@OptIn(UnsafeWasmMemoryApi::class) +internal fun readFile(path: String): ByteArray { + val (prefix, dirfd) = checkNotNull( + preloadedFds.filterKeys(path::startsWith).maxByOrNull { it.key.length } + ) { "file not found: $path" } + val fd = withScopedMemoryAllocator { allocator -> + val pathBytes = path.removePrefix(prefix).encodeToByteArray() + val pathPtr = allocator.allocate(pathBytes.size) + for ((i, b) in pathBytes.withIndex()) pathPtr.plus(i).storeByte(b) + val fdPtr = allocator.allocate(Int.SIZE_BYTES) + // fsRightsBase = fd_read + fd_seek + fd_tell + fd_filestat_get + val errno = pathOpen(dirfd, 1, pathPtr.address, pathBytes.size, 0, READ_RIGHTS, 0, 0, fdPtr.address) + check(errno == 0) { "path_open: $errno" } + fdPtr.loadInt() + } + try { + var size = 0UL + if (size == 0UL) withScopedMemoryAllocator { allocator -> + val filestatPtr = allocator.allocate(8 * Long.SIZE_BYTES) + val errno = fdFilestatGet(fd, filestatPtr.address) + if (errno == 0) size = filestatPtr.plus(4 * Long.SIZE_BYTES).loadLong().toULong() + } + if (size == 0UL) withScopedMemoryAllocator { allocator -> + val filesizePtr = allocator.allocate(Long.SIZE_BYTES) + val errno = fdSeek(fd, 0, 2, filesizePtr.address) + if (errno == 0) size = filesizePtr.loadLong().toULong() + } + withScopedMemoryAllocator { allocator -> + val iovsPtr = allocator.allocate(Long.SIZE_BYTES) + val sizePtr = allocator.allocate(Long.SIZE_BYTES) + var offset = 0UL + val buffers = buildList { + var bufsiz = if (size in 1UL..Int.MAX_VALUE.toULong()) size.toInt() else BUFSIZ + while (true) { + val bufferPtr = allocator.allocate(bufsiz) + iovsPtr.storeInt(bufferPtr.address.toInt()) + iovsPtr.plus(Int.SIZE_BYTES).storeInt(bufsiz) + var errno: Int + do { + errno = fdPread(fd, iovsPtr.address, 1, offset.toLong(), sizePtr.address) + } while (errno == 6) // errno.again + check(errno == 0) { "fd_pread: $errno" } + val size = sizePtr.loadLong().toULong() + check(size <= bufsiz.toULong()) { "fd_read: $size > $bufsiz" } + if (size == 0UL) break + add(bufferPtr to size.toInt()) + offset += size + bufsiz = BUFSIZ + check(offset <= Int.MAX_VALUE.toULong()) { "readFile: 22" } + } + } + val buffer = ByteArray(offset.toInt()) + buffers.fold(0) { acc, (bufferPtr, size) -> + repeat(size) { + buffer[acc + it] = bufferPtr.plus(it).loadByte() + } + acc + size + } + return buffer + } + } finally { + fdClose(fd) + } +} diff --git a/kt/aoc2024-exe/src/wasmWasiMain/kotlin/com/github/ephemient/aoc2024/exe/WasmWasiMain.kt b/kt/aoc2024-exe/src/wasmWasiMain/kotlin/com/github/ephemient/aoc2024/exe/WasmWasiMain.kt new file mode 100644 index 0000000..75d079a --- /dev/null +++ b/kt/aoc2024-exe/src/wasmWasiMain/kotlin/com/github/ephemient/aoc2024/exe/WasmWasiMain.kt @@ -0,0 +1,6 @@ +package com.github.ephemient.aoc2024.exe + +suspend fun main() { + val argv = argv() + mainImpl(argv.copyOfRange(2, argv.size)) +}