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

[WIP] Wasm support #95

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ buildconfig = "5.1.0"
gradle-publish = "1.2.1"
nexus-publish = "1.3.0"
detekt = "1.23.4"
nodejs = "18.14.1"
yarn = "1.22.19"
nodejs = "21.4.0"
yarn = "1.22.21"

[plugins]
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
Expand Down
13 changes: 13 additions & 0 deletions resources-library/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ kotlin {
nodejs()
}

wasmJs {
browser()
nodejs()
}

iosArm64()
iosX64()
iosSimulatorArm64()
Expand All @@ -60,6 +65,14 @@ kotlin {
}

val commonMain by getting
val jsMain by getting
val wasmJsMain by getting
val jsSharedMain by creating {
dependsOn(commonMain)
jsMain.dependsOn(this)
wasmJsMain.dependsOn(this)
}

val mingwX64Main by getting
val linuxX64Main by getting
val linuxArm64Main by getting
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,23 @@ import org.khronos.webgl.Int8Array
import org.khronos.webgl.Uint8Array
import org.w3c.xhr.XMLHttpRequest

private fun hasNodeApi(): Boolean = js("""{
(typeof process !== 'undefined'
&& process.versions != null
&& process.versions.node != null) ||
(typeof window !== 'undefined'
&& typeof window.process !== 'undefined'
&& window.process.versions != null
&& window.process.versions.node != null)
}"""
)

private fun readFileSync(path: String, options: String): String = js("require('fs').readFileSync(path, options)")

private fun readFileSync(path: String): Uint8Array = js("require('fs').readFileSync(path)")

private fun existsSync(path: String): Boolean = js("require('fs').existsSync(path)")

/*
* It's impossible to separate browser/node JS runtimes, as they can't be published separately.
* See: https://youtrack.jetbrains.com/issue/KT-47038
Expand All @@ -14,32 +31,27 @@ public actual class Resource actual constructor(path: String) {
private val resourceBrowser: ResourceBrowser by lazy { ResourceBrowser(path) }
private val resourceNode: ResourceNode by lazy { ResourceNode(path) }

public actual fun exists(): Boolean = when {
IS_BROWSER -> resourceBrowser.exists()
IS_NODE -> resourceNode.exists()
else -> throw UnsupportedOperationException("Unsupported JS runtime")
public actual fun exists(): Boolean = if (HAS_NODE_API) {
resourceNode.exists()
} else {
resourceBrowser.exists()
}

public actual fun readText(): String = when {
IS_BROWSER -> resourceBrowser.readText()
IS_NODE -> resourceNode.readText()
else -> throw UnsupportedOperationException("Unsupported JS runtime")
public actual fun readText(): String = if (HAS_NODE_API) {
resourceNode.readText()
} else {
resourceBrowser.readText()
}

public actual fun readBytes(): ByteArray = when {
IS_BROWSER -> resourceBrowser.readBytes()
IS_NODE -> resourceNode.readBytes()
else -> throw UnsupportedOperationException("Unsupported JS runtime")
public actual fun readBytes(): ByteArray = if (HAS_NODE_API) {
resourceNode.readBytes()
} else {
resourceBrowser.readBytes()
}

private companion object {
@Suppress("MaxLineLength")
private val IS_BROWSER: Boolean = js(
"typeof window !== 'undefined' && typeof window.document !== 'undefined' || typeof self !== 'undefined' && typeof self.location !== 'undefined'"
) as Boolean
private val IS_NODE: Boolean = js(
"typeof process !== 'undefined' && process.versions != null && process.versions.node != null"
) as Boolean
private val HAS_NODE_API: Boolean = hasNodeApi()
}

/*
Expand Down Expand Up @@ -80,31 +92,17 @@ public actual class Resource actual constructor(path: String) {
* Node-based resource implementation.
*/
private class ResourceNode(val path: String) {
val fs = nodeRequire("fs")

private fun nodeRequire(name: String): dynamic {
// Alternative to declaring `private external fun require(name: String): dynamic` and
// using `require("fs")` directly, since it will cause webpack to complain when running
// on the browser, even though the code is unused.

return try {
js("module['' + 'require']")(name)
} catch (e: dynamic) {
throw IllegalArgumentException("Module not found: $name", e as? Throwable)
}
}

fun exists(): Boolean = fs.existsSync(path) as Boolean
fun exists(): Boolean = existsSync(path)

fun readText(): String = runCatching {
fs.readFileSync(path, "utf8") as String
readFileSync(path, "utf8")
}.getOrElse { cause ->
throw FileReadException("$path: Read failed", cause)
}

fun readBytes(): ByteArray = runCatching {
val buffer = fs.readFileSync(path).unsafeCast<Uint8Array>()
Int8Array(buffer.buffer, buffer.byteOffset, buffer.length).unsafeCast<ByteArray>()
val buffer = readFileSync(path)
Int8Array(buffer.buffer, buffer.byteOffset, buffer.length) as ByteArray
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There must be a better way to do this.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@goncalossilva is this because unsafeCast is available only under the js* source sets?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unsafeCast is available on Wasm too, but its type must inherit from JsAny which ByteArray doesn't.

Copy link

@lppedd lppedd Dec 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@goncalossilva maybe you can introduce your own unsafeCast temporarily? It's just a call to asDynamic, although not sure how it works in WASM.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah just seen, it's implementedAsIntrinsic, so no luck on that.

Copy link

@lppedd lppedd Dec 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've just added support for wasmJs to antlr-kotlin and I've experimented a bit with it.

Working with primitive types in a shared way, with JS interop like this case, doesn't seem to be possible.
I ended up duplicating some of the code and using WASM types like JsBoolean and JsNumber, that you can get with, e.g.:

yourBool.toJsBoolean()

The js functions also have different signatures and compile time requirements, so they don't play well together.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need WASI file system APIs, I've been pointed to https://github.com/kowasm/kowasm
You'll need to copy-paste the WASM imports and Kotlin functions, but they should work.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lppedd nice share, thanks! I have some downtime during the holiday season, but I'll definitely be looking into these when I pick this back up.

}.getOrElse { cause ->
throw FileReadException("$path: Read failed", cause)
}
Expand Down
11 changes: 11 additions & 0 deletions resources-test/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,17 @@ kotlin {
nodejs()
}

wasmJs {
browser {
testTask {
useKarma {
useAnyBrowser()
}
}
}
nodejs()
}

iosArm64()
iosX64()
iosSimulatorArm64()
Expand Down