I’m experimenting with using Kotlin compilation to Wasm to run it in an embeddable Wasm runtime like WasmEdge. Rich data exchange usually happens through linear memory. For example, TinyGo compiled-guests export malloc and free functions which makes it possible for a host to allocate memory up front and then pass the pointer to it when calling a guest function.
Kotlin doesn’t export anything like malloc and it doesn’t seem to provide any means to implement it. Manual memory allocation can only happen inside a withScopedMemoryAllocator {} so how the host is supposed to pass any non-primitive data to a guest?
Is it supposed to be something like this?:
@OptIn(UnsafeWasmMemoryApi::class)
@WasmExport("func_with_string_arg")
fun funcWithStringArg(stringSize: Int) = withScopedMemoryAllocator { allocator ->
val ptr = allocator.allocate(stringSize)
storeData(ptr.address.toInt()) // this calls the host which should now store the string at the provided address
// read the data at ptr
}
This would mean the host would have to store some sort of context that should be available for the duration of the guest call. Is it the only option?
It’s also not clear how to return rich data out of a function call. If I understand correctly, all allocated memory will be garbage collected on exit so returning a pointer to it wouldn’t work.
@bashor, thanks, so to clarify the only way to return rich data from a Kotlin guest function call would be to call some host function that would store the result, i.e. something like this?
fun funcWithStringArg(stringSize: Int, callId Int) = withScopedMemoryAllocator { allocator ->
// allocate a buffer for the argument
callHostToStoreArgumentData(argBufPtr.address.toInt(), callId) // host can understand by callId arguments of which call are requested
// execute the function body
// store the function result data in the linear memory
callHostToReturnResultData(resultDataPtr.address.toInt(), callId) // host can understand by callId which call returned the result
}
Is it possible in the future there will be other options, maybe something like a global allocator that allows allocations that can outlive a function call?
Yes, it’s the proper way right now.
Basically, it’s not that bad – execution will cross a host-guest boundary the same times as if we had exported allocate function.
It’s not like only way, but others may have their own disadvantages, depending on your case, e.g.
you can define and export array_get(array, index), array_set(array, index, value) function and use GC array, or maybe your VM provide API to work with GC arrays out of the box.
you can rely on some implementation details of Kotlin’s allocator, but it will require you to be more accurate and keep in mind that implementation details may change in the future.
Maybe, but no ETAs.
Something close to your needs should be done for Component Model, see KT-65030.
Aside from that, I’ve created a bunch of issues around memory, following ones could be interesting for your case:
KT-73261 K/Wasm: provide an Allocator API not limited by scope
All low-level functions for working with memory that Kotlin’s allocator use are marked as internal as far as I can see so not sure if it would be possible to implement such functions at the moment.
Thank you, I will keep an eye on the issues listed!