[Wasm][Stdlib] Add public APIs for linear memory access

Needed for interop with APIs that use linear memory.
This commit is contained in:
Svyatoslav Kuzmich
2022-12-29 19:25:23 +00:00
committed by Space Team
parent fbf06b5495
commit 9bc6b420a9
20 changed files with 702 additions and 56 deletions
@@ -0,0 +1,18 @@
/*
* Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package kotlin.wasm.unsafe
import kotlin.annotation.AnnotationTarget.*
/**
* This annotation marks APIs for working with unmanaged WebAssembly linear memory.
*
* Any usage of a declaration annotated with `@UnsafeWasmMemoryApi` must be accepted either by
* annotating that usage with the [OptIn] annotation, e.g. `@OptIn(UnsafeWasmMemoryApi::class)`,
* or by using the compiler argument `-opt-in=kotlin.wasm.unsafe.UnsafeWasmMemoryApi`.
*/
@RequiresOptIn("Unsafe APIs to access to WebAssembly linear memory")
public annotation class UnsafeWasmMemoryApi
@@ -0,0 +1,73 @@
/*
* Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package kotlin.wasm.unsafe
import kotlin.wasm.internal.WasmOp
import kotlin.wasm.internal.implementedAsIntrinsic
/**
* Linear memory pointer type.
* Corresponds to `i32` type on 32-bit Wasm architecture.
*/
@UnsafeWasmMemoryApi
public value class Pointer public constructor(public val address: UInt) {
/** Adds an [Int] to the address of this [Pointer] */
public operator fun plus(other: Int): Pointer =
Pointer(address + other.toUInt())
/** Subtracts an [Int] from the address of this [Pointer] */
public operator fun minus(other: Int): Pointer =
Pointer(address - other.toUInt())
/** Adds an [UInt] to the address of this [Pointer] */
public operator fun plus(other: UInt): Pointer =
Pointer(address + other)
/** Subtracts an [UInt] from the address of this [Pointer] */
public operator fun minus(other: UInt): Pointer =
Pointer(address - other)
/** Load a Byte (8 bit) value */
@WasmOp(WasmOp.I32_LOAD8_S)
public fun loadByte(): Byte =
implementedAsIntrinsic
/** Load a Short (16 bit) value */
@WasmOp(WasmOp.I32_LOAD16_S)
public fun loadShort(): Short =
implementedAsIntrinsic
/** Load an Int (32 bit) value */
@WasmOp(WasmOp.I32_LOAD)
public fun loadInt(): Int =
implementedAsIntrinsic
/** Load a Long (64 bit) value */
@WasmOp(WasmOp.I64_LOAD)
public fun loadLong(): Long =
implementedAsIntrinsic
/** Store a Byte (8 bit) [value] */
@WasmOp(WasmOp.I32_STORE8)
public fun storeByte(value: Byte): Unit =
implementedAsIntrinsic
/** Store a Short (16 bit) [value] */
@WasmOp(WasmOp.I32_STORE16)
public fun storeShort(value: Short): Unit =
implementedAsIntrinsic
/** Store an Int (32 bit) [value] */
@WasmOp(WasmOp.I32_STORE)
public fun storeInt(value: Int): Unit =
implementedAsIntrinsic
/** Store a Long (64 bit) [value] */
@WasmOp(WasmOp.I64_STORE)
public fun storeLong(value: Long): Unit =
implementedAsIntrinsic
}
@@ -0,0 +1,156 @@
/*
* Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package kotlin.wasm.unsafe
import kotlin.wasm.internal.WasmOp
import kotlin.wasm.internal.implementedAsIntrinsic
import kotlin.wasm.internal.unsafeGetScratchRawMemory
import kotlin.contracts.*
/**
* WebAssembly linear memory allocator.
*/
@UnsafeWasmMemoryApi
public abstract class MemoryAllocator {
/**
* Allocates a block of uninitialized linear memory of the given [size] in bytes.
*
* @return an address of allocated memory. It is guaranteed to be a multiple of 8.
*/
public abstract fun allocate(size: Int): Pointer
}
/**
* Runs the [block] of code, providing it a temporary [MemoryAllocator] as an argument, and returns the result of this block.
*
* Frees all memory allocated with the provided allocator after running the [block].
*
* This function is intened to facilitate the exchange of values with outside world through linear memory.
* For example:
*
* ```
* val buffer_size = ...
* withScopedMemoryAllocator { allocator ->
* val buffer_address = allocator.allocate(buffer_size)
* importedWasmFunctionThatWritesToBuffer(buffer_address, buffer_size)
* return readDataFromBufferIntoManagedKotlinMemory(buffer_address, buffer_size)
* }
* ```
*
* WARNING! Addresses allocated inside the [block] function become invalid after exiting the function.
*
* WARNING! A nested call to [withScopedMemoryAllocator] will temporarily disable the allocator from the outer scope
* for the duration of the call. Calling [MemoryAllocator.allocate] on a disabled allocator
* will throw [IllegalStateException].
*
* WARNING! Accessing the allocator outside of the [block] scope will throw [IllegalStateException].
*/
@UnsafeWasmMemoryApi
public inline fun <T> withScopedMemoryAllocator(
block: (allocator: MemoryAllocator) -> T
): T {
contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
val allocator = createAllocatorInTheNewScope()
val result = try {
block(allocator)
} finally {
allocator.destroy()
currentAllocator = allocator.parent
}
return result
}
@PublishedApi
@UnsafeWasmMemoryApi
internal fun createAllocatorInTheNewScope(): ScopedMemoryAllocator {
val allocator = currentAllocator?.createChild() ?:
ScopedMemoryAllocator(unsafeGetScratchRawMemory(), parent = null)
currentAllocator = allocator
return allocator
}
@PublishedApi
@UnsafeWasmMemoryApi
internal var currentAllocator: ScopedMemoryAllocator? = null
@PublishedApi
@UnsafeWasmMemoryApi
internal class ScopedMemoryAllocator(
startAddress: Int,
// Allocator from parent scope or null for top-level scope.
@PublishedApi
internal var parent: ScopedMemoryAllocator?,
) : MemoryAllocator() {
// true if allocator is out of scope
private var destroyed = false
// true if child allocator is active
private var suspended = false
// all memory is available starting from this address
private var availableAddress: ULong = startAddress.toULong()
override fun allocate(size: Int): Pointer {
check(!destroyed) { "ScopedMemoryAllocator is destroyed when out of scope" }
check(!suspended) { "ScopedMemoryAllocator is suspended when nested allocators are used" }
// Pad available address to align it to 8
// 8 is a max alignment number currently needed for Wasm component model canonical ABI
val align = 8uL
val result = (availableAddress + align - 1uL) and (align - 1uL).inv()
check(result % 8uL == 0uL)
availableAddress = result + size.toULong()
if (availableAddress > UInt.MAX_VALUE.toULong()) {
error("Out of linear memory. All available address space (4gb) is used.")
}
val currentMaxSize = wasmMemorySize().toULong() * WASM_PAGE_SIZE_IN_BYTES.toULong()
if (availableAddress >= currentMaxSize) {
val numPagesToGrow =
(availableAddress - currentMaxSize) / WASM_PAGE_SIZE_IN_BYTES.toULong() + 2uL
if (wasmMemoryGrow(numPagesToGrow.toInt()) == -1) {
error("Out of linear memory. memory.grow returned -1")
}
}
check(availableAddress < wasmMemorySize().toULong() * WASM_PAGE_SIZE_IN_BYTES.toULong())
return Pointer(result.toUInt())
}
@PublishedApi
internal fun createChild(): ScopedMemoryAllocator {
val child = ScopedMemoryAllocator(availableAddress.toInt(), parent = this)
suspended = true
return child
}
@PublishedApi
internal fun destroy() {
destroyed = true
parent?.suspended = false
}
}
private const val WASM_PAGE_SIZE_IN_BYTES = 65_536 // 64 KiB
/**
* Current linear memory size in pages
*/
@WasmOp(WasmOp.MEMORY_SIZE)
internal fun wasmMemorySize(): Int =
implementedAsIntrinsic
/**
* Grow memory by a given delta (in pages).
* Return the previous size, or -1 if enough memory cannot be allocated.
*/
@WasmOp(WasmOp.MEMORY_GROW)
internal fun wasmMemoryGrow(delta: Int): Int =
implementedAsIntrinsic