Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Get WASI set up enough to do a directory list #1297

Merged
merged 11 commits into from
Jul 21, 2023
12 changes: 12 additions & 0 deletions okio-wasifilesystem/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
WASI FileSystem
===============

⚠️ This is a work in progress ⚠️

This module implements Okio's FileSystem API using the [WebAssembly System Interface (WASI)][wasi].

It currently uses the WASI [preview1] APIs and is tested on NodeJS with the
`--experimental-wasi-unstable-preview1` option.

[wasi]: https://wasi.dev/
[preview1]: https://github.com/WebAssembly/WASI/blob/main/legacy/preview1/docs.md
104 changes: 104 additions & 0 deletions okio-wasifilesystem/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import com.vanniktech.maven.publish.JavadocJar.Dokka
import com.vanniktech.maven.publish.KotlinMultiplatform
import com.vanniktech.maven.publish.MavenPublishBaseExtension
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinJsTest

plugins {
kotlin("multiplatform")
id("org.jetbrains.dokka")
id("com.vanniktech.maven.publish.base")
id("build-support")
id("binary-compatibility-validator")
}

kotlin {
configureOrCreateWasmPlatform()
sourceSets {
all {
languageSettings.optIn("kotlin.wasm.unsafe.UnsafeWasmMemoryApi")
}
val wasmMain by getting {
dependencies {
implementation(projects.okio)
}
}
val wasmTest by getting {
dependencies {
implementation(projects.okioTestingSupport)
implementation(libs.kotlin.test)
}
}
}
}

configure<MavenPublishBaseExtension> {
configure(
KotlinMultiplatform(javadocJar = Dokka("dokkaGfm")),
)
}

/**
* Inspired by runner.mjs in kowasm, this rewrites the JavaScript bootstrap script to set up WASI.
*
* See also:
* * https://github.com/kowasm/kowasm
* * https://github.com/nodejs/node/blob/main/doc/api/wasi.md
*
* This task overwrites the output of `compileTestKotlinWasm` and must run after that task. It
* must also run before the WASM test execution tasks that read this script.
*
* Note that this includes which file paths are exposed to the WASI sandbox.
*/
val injectWasiInit by tasks.creating {
dependsOn("compileTestKotlinWasm")
val moduleName = "${rootProject.name}-${project.name}-wasm-test"

val entryPointMjs = File(
buildDir,
"compileSync/wasm/test/testDevelopmentExecutable/kotlin/$moduleName.mjs"
)

outputs.file(entryPointMjs)

doLast {
val tmpdir = File(System.getProperty("java.io.tmpdir"), "okio-wasifilesystem-test")
tmpdir.mkdirs()

entryPointMjs.writeText(
"""
import {instantiate} from './$moduleName.uninstantiated.mjs';
import {WASI} from "wasi";

export const wasi = new WASI({
version: 'preview1',
preopens: {
'/tmp': '$tmpdir'
}
});

const {instance, exports} = await instantiate({wasi_snapshot_preview1: wasi.wasiImport});

wasi.initialize(instance);

export default exports;
""".trimIndent()
)
}
}
tasks.withType<KotlinJsTest>().configureEach {
// TODO: get this working on Windows.
// > command 'C:\Users\runneradmin\.gradle\nodejs\node-v20.0.0-win-x64\node.exe'
// exited with errors (exit code: 1)
onlyIf {
val os = DefaultNativePlatform.getCurrentOperatingSystem()
!os.isWindows
}
nodeJsArgs += "--experimental-wasi-unstable-preview1"
}
tasks.named("wasmTestTestDevelopmentExecutableCompileSync").configure {
dependsOn(injectWasiInit)
}
tasks.named("wasmTestTestProductionExecutableCompileSync").configure {
dependsOn(injectWasiInit)
}
87 changes: 87 additions & 0 deletions okio-wasifilesystem/src/wasmMain/kotlin/okio/FileSink.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* Copyright (C) 2023 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okio

import kotlin.wasm.unsafe.withScopedMemoryAllocator
import okio.internal.ErrnoException
import okio.internal.fdClose
import okio.internal.preview1.fd
import okio.internal.preview1.fd_write
import okio.internal.preview1.size
import okio.internal.write

internal class FileSink(
private val fd: fd,
) : Sink {
private var closed = false
private val cursor = Buffer.UnsafeCursor()

override fun write(source: Buffer, byteCount: Long) {
require(byteCount >= 0L) { "byteCount < 0: $byteCount" }
require(source.size >= byteCount) { "source.size=${source.size} < byteCount=$byteCount" }
check(!closed) { "closed" }

var bytesRemaining = byteCount
source.readAndWriteUnsafe(cursor)
try {
while (bytesRemaining > 0L) {
check(cursor.next() != -1)

val count = minOf(bytesRemaining, cursor.end.toLong() - cursor.start).toInt()
if (fdWrite(cursor.data!!, cursor.start, count) != count) {
throw IOException("write failed")
}
bytesRemaining -= count
}
} finally {
cursor.close()
source.skip(byteCount)
}
}

private fun fdWrite(data: ByteArray, offset: Int, count: Int): size {
withScopedMemoryAllocator { allocator ->
val dataPointer = allocator.write(data, offset, count)

val iovec = allocator.allocate(8)
iovec.storeInt(dataPointer.address.toInt())
(iovec + 4).storeInt(count)

val returnPointer = allocator.allocate(4) // `size` is u32, 4 bytes.
val errno = fd_write(
fd = fd,
iovs = iovec.address.toInt(),
iovsSize = 1,
returnPointer = returnPointer.address.toInt(),
)
if (errno != 0) throw ErrnoException(errno.toShort())

return returnPointer.loadInt()
}
}

override fun flush() {
// TODO
}

override fun timeout() = Timeout.NONE

override fun close() {
if (closed) return
closed = true
fdClose(fd)
}
}
Loading