Skip to content

Commit

Permalink
Merge pull request #56 from ephemient/kt/wasm
Browse files Browse the repository at this point in the history
  • Loading branch information
ephemient authored Dec 6, 2024
2 parents 1c2bd71 + 122bde9 commit 38481bf
Show file tree
Hide file tree
Showing 8 changed files with 239 additions and 9 deletions.
29 changes: 26 additions & 3 deletions .github/workflows/kt.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion kt/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
30 changes: 27 additions & 3 deletions kt/aoc2024-exe/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ plugins {
distribution
}

class UpdateWasmWrapper(private val mainFile: Provider<RegularFile>) : Action<Task> {
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(
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package com.github.ephemient.aoc2024.exe

private fun argv(): String = js("process.argv.join(' ')")
private fun argv(): JsArray<JsString> = js("process.argv")

suspend fun main() {
mainImpl(argv().split(' ').drop(2).toTypedArray())
val argv = argv()
mainImpl(Array(argv.length - 2) { argv[it + 2].toString() })
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.github.ephemient.aoc2024.exe

internal actual fun getDayInput(day: Int): String = readFile("/data/day$day.txt").decodeToString()
Original file line number Diff line number Diff line change
@@ -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<String> {
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")
}
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.github.ephemient.aoc2024.exe

suspend fun main() {
val argv = argv()
mainImpl(argv.copyOfRange(2, argv.size))
}

0 comments on commit 38481bf

Please sign in to comment.