Hi Everyone! I know this is a tricky topic as it i...
# webassembly
a
Hi Everyone! I know this is a tricky topic as it is, but has anyone found a livable solution for transferring AND accessing kotlin classes in JS using the WASM target or a combination of WASM / JS? Currently we're generating
toJs()
methods on classes that translate them to a JsObject external interface (a normal JS object, not a class). This is great in terms of managing model and logic in WASM, but obviously limited and a little costly on the frontend, though at this point it doesn't seem to be user noticeable. The limitations of the js() method, not being able to export classes, etc. has been a huge pain at every turn, so I'm hoping someone can shed some light on the balance they've found. We're going to try testing exporting common classes in the JS target and using them from the WASM target, but I fear that will be a complex mess to end up with friendlier code but much worse performance. All ideas and experience are welcome and we'd love to exchange thoughts. We're still learning the costs and trade-offs of using WASM (as everyone is).
t
Do you have example of
toJs
method?
a
Supporting code...This maps the JS Object class via an external interface
JSObject
and has the base functions for type handling etc. This is a test version of the code to prove the concept that will be more useful to you to drop in. Note the
JsObjectClass
interface at the bottom that we'll use with our classes.
Copy code
package io.codetactics.web.editor.model.js

@JsName("Object")
external class JsObject : JsAny {
	operator fun get(key: JsString): JsAny?
	operator fun set(key: JsString, value: JsAny?)

	@JsName("hasOwnProperty")
	fun has(key: JsString): Boolean
}

inline fun <T : JsAny> makeJsObject(init: T.() -> Unit): T = (JsObject().unsafeCast<T>()).apply(init)

fun Any.jsValueOf() = jsValueOf(this)

@Suppress("UNCHECKED_CAST")
fun jsValueOf(value: Any?): JsAny = when(value) {
	is JsObjectClass -> value.toJsObject()
	is String -> value.toJsString()
	is Boolean -> value.toJsBoolean()
	is Int -> value.toJsNumber()
	is Long -> value.toJsBigInt()
	is Double -> value.toJsNumber()
	is List<*> -> value.map(::jsValueOf).toJsArray()
	is Map<*, *> -> (value as Map<String, Any?>).entries.fold(JsObject()) { result, (key, value) ->
		result.apply {
			set(key.toJsString(), jsValueOf(value))
		}
	}
	else -> error("Unsupported type: ${value?.let { it::class.simpleName }}")
}

interface JsObjectClass {
	fun toJsObject() = JsObject()
}
Then, you can have a class that implements
JsObjectClass
like so:
Copy code
@Serializable
data class AnythingResponse(
	var args: Map<String, String>,
	var data: String,
	var files: Map<String, String>,
	var form: Map<String, String>,
	var headers: Map<String, String>,
	var json: Map<String, String>?,
	var method: String,
	var origin: String,
	var url: String
) : JsObjectClass {
	override fun toJsObject() = JsObject().apply {
		set("args".toJsString(), args.jsValueOf())
		set("data".toJsString(), data.jsValueOf())
		set("files".toJsString(), files.jsValueOf())
		set("form".toJsString(), form.jsValueOf())
		set("headers".toJsString(), headers.jsValueOf())
		set("json".toJsString(), json?.jsValueOf())
		set("method".toJsString(), method.jsValueOf())
		set("origin".toJsString(), origin.jsValueOf())
		set("url".toJsString(), url.jsValueOf())
	}
}
t
We have parallel thread, where we discuss JS interop, but looks like we have common pill for both cases. OpenAPI can generate all required types and safe builders. cc @Sergei Grishchenko
a
Yes, I've seen some attempts at implementing the model / DTOs / value classes in the commonMain target, and trying to make a generic
@JsExports
note the 's' annotation that applies to classes in both the js target and wasm target. It seems like a total mess and I've not seen any confirmation that it actually solves the problem, but like you said, you then end up with multiple copies of classes and you still have to make sure the WASM target knows that it can create those classes in JS. And even if all that worked, you'd still be out of luck on your generic list haha. For all the projects talking about the potential, the lack of translating even simple classes pretty much cripples WASM in a web client for us. I have no doubt jetbrains is working to make it happen, but its a problem of getting folks excited about it and posting creatively around the massive hole in functionality, while still trying to suggest its incredibly usable right now. I just feel that was a strategy mistake or perhaps its other bloggers and such looking for hype that made the mistake for jetbrains.
t
Looks like you need intensive communication between JS and WASM. What is your use case? 🤔
a
Well, the hope was that we would be able to do it strictly in the WASM target and have access to at least basic properties in JS which we can get away with. We're building a specialized rich document type editor, but would use this same framework for pretty much anything else. The model is at the core of most applications, so not being able to share that model between two technologies drastically limits the effectiveness of it. All that said, I have to be fair and say the performance of the current JS Object conversion still seems quite reasonable so far.
How about you?
Oh, and Kotlinx Serialization can parse generic typed arrays if you give it enough to discern the type, but maybe your parsing a polymorphic generic type. I haven't tried it on wasm, but we use generic lists in a lot of projects with kotlinx serialization and almost never use the context aware annotations. If you like, I'll check for an example in our code.
t
From my perspective we miss 3 additional serializers: 1. Kotlin/WASM data class instance to JSO (with WASM transfer rules respect) 2. Kotlin/JS data class instance to JSO (with JS transfer rules respect) 3. Kotlin/JS data class instance to JSO (with respect for platform types)
Json
in current cases is workaround unfortunatelly, but probably it will work in your cases
👍 1
And all serializers require more use cases (like yours)
👍 1
a
Your cases are exactly why we had to make a custom interface. All our models are mostly data classes as well so this solves the JSO part of the equation, but we don't like having to right the when based type conversion. There's should be an easy way to at least get a JSO from a data class if not actually use the class properties in JS, but I get that its not always an easy proposition to build.
t
But what you will do on JS side?
In common case you can use data class on both platforms (with serialization/serialization)
Any transport will be fine in that case
a
At this point we may just not use it if we run into further problems. We're testing what its like working around the class/object problem this way and we'll likely just not have complete classes represented on the JS side. This is a pretty serious weakness to us and signifies that its just too early as much as we would like to use it, so we'll see what the final verdict is. I appreciate the work JB has put into this for sure, but I feel it got a bit oversold by folks that were excited about it.
t
For simple use case “Kotlin on JS side + Kotlin on WASM side” you already have default solution - Common DTOs + serialization.
But as I understood - you also want to export JSO interfaces in TS? Right?
I feel it got a bit oversold by folks that were excited about it
Is there links on videos with excited users? 🙂
a
Haha, mostly blogs with very contrived examples that don't mention the lack of fairly simplistic needs (ie: they show what works and avoid showing the obvious that doesn't work). I don't do videos for programming much as it moves a little slow for me and lacks search, but that's just my preference. Regarding our situation, we are still not at a functionally acceptable place yet. The way WASM and JS in KMP are, even trying to get javascript and kotlin versions of the same class is a pretty complex mess that we haven't seen fully work yet either. Our best move is the JSObject generation we did earlier. If that allows us to write the more complex logic in Kotlin without another show-stopping problem, we may have a path for this project at least. But requiring a custom code-generation on KSP just to get basic functionality that isn't even complete enough to call it basic is a bit rough for most users though we didn't mind it to give this a shot knowing we're pushing the edges of this. For clarity, once again, I don't want to come off as bashing WASM. I understand its early and we've been on Kotlin since its beginning, so we're more than familiar with how this stuff goes. This one is just a pretty large time suck to even get started so we're trying to find the use it or cut bait point. It will get where it needs to go eventually.