[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,184 @@
package test.wasm.unsafe
import kotlin.wasm.unsafe.*
import kotlin.test.*
@OptIn(UnsafeWasmMemoryApi::class)
class MemoryAccessTestTest {
@Test
fun testPointer() {
val p: Pointer = Pointer(19u)
assertEquals(p.address, 19u)
assertEquals((p + 10u).address, 29u)
assertEquals((p - 10u).address, 9u)
assertEquals((p + 10).address, 29u)
assertEquals((p - 10).address, 9u)
}
fun <T> testLoadStore(values: List<T>, typeSize: Int, store: (Pointer, T) -> Unit, load: (Pointer) -> T) {
withScopedMemoryAllocator { a ->
// Memory layout: [Long1][T][Long2]
val ptrToLong1 = a.allocate(24)
val ptrToT = ptrToLong1 + 8
val ptrToLong2 = ptrToT + typeSize
for (x in values) {
val prevLong = ptrToLong1.loadLong()
val nextLong = ptrToLong2.loadLong()
store(ptrToT, x)
assertEquals(load(ptrToT), x)
assertEquals(ptrToLong1.loadLong(), prevLong)
assertEquals(ptrToLong2.loadLong(), nextLong)
}
}
}
@Test
fun testByte() {
val bytes = listOf<Byte>(0, Byte.MIN_VALUE, Byte.MAX_VALUE, -91, -21, -47, -72, -42, 118, 120, 125, 21, -43)
testLoadStore(bytes, 1, { p, v -> p.storeByte(v) }, { it.loadByte() })
}
@Test
fun testShort() {
val shorts = listOf<Short>(
0, Short.MIN_VALUE, Short.MAX_VALUE, 26350, 17667, 5437, 1381,
21183, 26042, -25961, -22913, 9128, -10684
)
testLoadStore(shorts, 2, { p, v -> p.storeShort(v) }, { it.loadShort() })
}
@Test
fun testInt() {
val ints = listOf<Int>(
0, Int.MIN_VALUE, Int.MAX_VALUE,
1348618689, -299556943, -394977414, -621300994, 1034622853, -1010496662,
-2102993550, 199131417, 407819728, -1093382545
)
testLoadStore(ints, 4, { p, v -> p.storeInt(v) }, { it.loadInt() })
}
@Test
fun testLong() {
val longs = listOf<Long>(
0, Long.MIN_VALUE, Long.MAX_VALUE,
6964777768087685094, -3965399897925814666, 6876207943944046195, 7675081221595661767,
-3388229176969119769, 4265730675328983821, -4893379785828386453, -7516879919690485136,
8512965883914804069, 6155050932825287650
)
testLoadStore(longs, 8, { p, v -> p.storeLong(v) }, { it.loadLong() })
}
@Test
fun testAccessingWithDifferentTypes() {
withScopedMemoryAllocator { a ->
val size = 16
val sizeU = size.toUInt()
val pointer = a.allocate(size)
val addr = pointer.address
fun fillWith(value: Byte) {
for (ptr in addr..<addr + sizeU) {
Pointer(ptr).storeByte(value)
}
}
fun fillWith(value: Short) {
for (ptr in addr..<addr + sizeU step 2) {
Pointer(ptr).storeShort(value)
}
}
fun fillWith(value: Int) {
for (ptr in addr..<addr + sizeU step 4) {
Pointer(ptr).storeInt(value)
}
}
fun fillWith(value: Long) {
for (ptr in addr..<addr + sizeU step 8) {
Pointer(ptr).storeLong(value)
}
}
fun checkMem(
bytes: List<Byte>,
shorts: List<Short>,
ints: List<Int>,
longs: List<Long>
) {
assertEquals(bytes.size, size)
assertEquals(shorts.size, size / 2)
assertEquals(ints.size, size / 4)
assertEquals(longs.size, size / 8)
for (i in 0..<size) {
assertEquals((pointer + i).loadByte(), bytes[i])
}
for (i in 0..<size / 2) {
assertEquals((pointer + i * 2).loadShort(), shorts[i])
}
for (i in 0..<size / 4) {
assertEquals((pointer + i * 4).loadInt(), ints[i])
}
for (i in 0..<size / 8) {
assertEquals((pointer + i * 8).loadLong(), longs[i])
}
}
fun checkZero() {
checkMem(
bytes = List(size) { 0 },
shorts = List(size / 2) { 0 },
ints = List(size / 4) { 0 },
longs = List(size / 8) { 0L }
)
}
fillWith(0.toByte())
checkZero()
fillWith(0x0F.toByte())
checkMem(
bytes = List(size) { 0x0F },
shorts = List(size / 2) { 0x0F0F },
ints = List(size / 4) { 0x0F0F0F0F },
longs = List(size / 8) { 0x0F0F0F0F0F0F0F0FL }
)
fillWith(0.toShort())
checkZero()
fillWith(0xABCDu.toShort())
checkMem(
bytes = mutableListOf<Byte>().also { list ->
repeat(size / 2) {
// little-endian
list += 0xCD.toByte()
list += 0xAB.toByte()
}
},
shorts = List(size / 2) { 0xABCDu.toShort() },
ints = List(size / 4) { 0xABCDABCDu.toInt() },
longs = List(size / 8) { 0xABCDABCDABCDABCDuL.toLong() }
)
fillWith(0.toInt())
checkZero()
fillWith(0xFFFFFFFFu.toInt())
checkMem(
bytes = List(size) { 0xFFu.toByte() },
shorts = List(size / 2) { 0xFFFFu.toShort() },
ints = List(size / 4) { 0xFFFFFFFFu.toInt() },
longs = List(size / 8) { 0xFFFFFFFFFFFFFFFFuL.toLong() }
)
fillWith(0L)
checkZero()
fillWith(0x1212121212121212L)
checkMem(
bytes = List(size) { 0x012 },
shorts = List(size / 2) { 0x1212 },
ints = List(size / 4) { 0x12121212 },
longs = List(size / 8) { 0x1212121212121212L }
)
}
}
}
@@ -0,0 +1,217 @@
package test.wasm.unsafe
import kotlin.wasm.unsafe.*
import kotlin.test.*
@JsFun("(a, b) => a + b")
private external fun jsConcatStrings(a: String, b: String): String
@OptIn(UnsafeWasmMemoryApi::class)
class MemoryAllocationTest {
val pageSize = 65_536
@Test
fun testWasmMemorySizeGrow() {
val s1 = wasmMemorySize()
val grow_res = wasmMemoryGrow(10)
val s2 = wasmMemorySize()
assertNotEquals(grow_res, -1)
assertEquals(grow_res, s1)
assertEquals(s2 - s1, 10)
}
@Test
fun testScopedAllocator() {
val sizes = listOf<Int>(1, 1, 2, 3, 8, 10, 305, 12_747, 31_999)
val allocations = mutableListOf<Pointer>()
withScopedMemoryAllocator { a ->
for (size in sizes) {
allocations += a.allocate(size)
}
}
val allocations2 = mutableListOf<Pointer>()
withScopedMemoryAllocator { a ->
for (size in sizes) {
allocations2 += a.allocate(size)
}
}
// Test that we run withScopedMemoryAllocator body
assertEquals(allocations.size, sizes.size)
// Memory should be reusing in different scopes
// NOTE: This is current impl detail and can be changed
assertEquals(allocations, allocations2)
// Allocations are aligned
assertTrue(allocations.all { it.address % 8u == 0u })
// Allocations are different
assertTrue(allocations.distinct().size == allocations.size)
// Allocations do not intersect
for (i1 in 0..<sizes.size) {
for (i2 in (i1 + 1)..<sizes.size) {
val a1 = allocations[i1].address
val a2 = allocations[i2].address
val size1 = sizes[i1].toUInt()
val size2 = sizes[i2].toUInt()
assertTrue(a1 !in a2..<a2 + size2)
assertTrue(a1 + size1 - 1u !in a2..<a2 + size2)
}
}
}
@Test
fun testScopedAllocatorGrowsMemory() {
// Allocations past current memory size should grow memory
val memSizes = mutableListOf<Int>(wasmMemorySize())
withScopedMemoryAllocator { a ->
var allocatedAddress = a.allocate(pageSize)
var allocationSize = pageSize
repeat(10) {
var currPagesUsed = (allocatedAddress.address.toInt() + allocationSize + 1) / pageSize
var currPagesAvailable = wasmMemorySize()
assertTrue(currPagesAvailable > currPagesUsed)
// Allocate 10 pages past max page
allocationSize = (currPagesAvailable - currPagesUsed + 10) * pageSize
allocatedAddress = a.allocate(allocationSize)
memSizes += wasmMemorySize()
}
}
assertTrue(memSizes.size == 11)
assertEquals(memSizes.distinct(), memSizes)
assertEquals(memSizes.sorted(), memSizes)
}
@Test
fun nestedAllocators() {
val sizes = listOf<Int>(1, 1, 2, 3, 8, 10, 305, 12_747, 31_999)
var allocations1: List<Pointer>
var allocations1_1: List<Pointer>
var allocations1_2: List<Pointer>
var allocations2: List<Pointer>
withScopedMemoryAllocator { allocator0 ->
allocations1 = sizes.map { size -> allocator0.allocate(size) }
withScopedMemoryAllocator { allocator0_0 ->
allocations1_1 = sizes.map { size -> allocator0_0.allocate(size) }
}
withScopedMemoryAllocator { allocator0_1 ->
allocations1_2 = sizes.map { size -> allocator0_1.allocate(size) }
}
}
withScopedMemoryAllocator { allocator0 ->
allocations2 = sizes.map { size -> allocator0.allocate(size) }
}
// Check that all allocations happened and distinct
listOf(
allocations1,
allocations1_1,
allocations1_2,
allocations2
).forEach { allocation ->
assertEquals(allocation.size, sizes.size)
assertEquals(allocation.distinct().size, allocation.size)
}
// Impl detail: sibling scopes use the same memory
assertEquals(allocations1, allocations2)
assertEquals(allocations1_1, allocations1_2)
// Impl detaiol: allocator in child scope allocates new memory
val max1: UInt = allocations1.maxOf { it.address }
val min1_1: UInt = allocations1_1.minOf { it.address }
assertTrue(max1 < min1_1)
}
@Test
fun testJsIntropInsideAllocations() {
withScopedMemoryAllocator { allocator ->
assertEquals(jsConcatStrings("str1", "str2"), "str1str2")
allocator.allocate(10)
}
}
@Test
fun testNestedAllocatorThrows() {
var leakedAllocator1: MemoryAllocator? = null
var leakedAllocator2: MemoryAllocator? = null
var leakedAllocator3: MemoryAllocator? = null
withScopedMemoryAllocator { allocator1 ->
leakedAllocator1 = allocator1
allocator1.allocate(100)
// 2-level nesting
withScopedMemoryAllocator { allocator2 ->
leakedAllocator2 = allocator2
allocator2.allocate(100)
assertFailsWith<IllegalStateException> {
allocator1.allocate(100)
}
// 3-level nesting
withScopedMemoryAllocator { allocator3 ->
leakedAllocator3 = allocator3
allocator3.allocate(100)
assertFailsWith<IllegalStateException> {
allocator1.allocate(100)
}
assertFailsWith<IllegalStateException> {
allocator2.allocate(100)
}
allocator3.allocate(100)
}
assertFailsWith<IllegalStateException> {
leakedAllocator3?.allocate(100)
}
// now it is legal to use allocator1 since we're in its immediate scope
allocator2.allocate(100)
}
assertFailsWith<IllegalStateException> {
leakedAllocator2?.allocate(100)
}
allocator1.allocate(100)
}
for (leakedAllocator in listOf(leakedAllocator1, leakedAllocator2, leakedAllocator3)) {
assertFailsWith<IllegalStateException> {
leakedAllocator?.allocate(100)
}
}
}
@Test
fun testScopedAllocatorThrows() {
assertFailsWith<IllegalStateException> {
var leakedAllocator: MemoryAllocator? = null
withScopedMemoryAllocator { allocator ->
leakedAllocator = allocator
}
leakedAllocator?.allocate(10)
}
assertFailsWith<IllegalStateException> {
var leakedAllocator: MemoryAllocator? = null
try {
withScopedMemoryAllocator { allocator ->
leakedAllocator = allocator
throw Error()
}
} catch (e: Throwable) {
}
leakedAllocator?.allocate(10)
}
assertFailsWith<IllegalStateException> {
fun foo(): MemoryAllocator {
withScopedMemoryAllocator { allocator ->
return allocator // non-local return
}
}
foo().allocate(10)
}
}
}