From 2cd9e7f3df84ec743212110288e9a75fcab70252 Mon Sep 17 00:00:00 2001 From: lax1dude Date: Wed, 30 Oct 2024 00:08:07 -0700 Subject: [PATCH] work on LaxMalloc --- .../java/org/teavm/runtime/LaxMalloc.java | 285 ++++++++++++++++++ 1 file changed, 285 insertions(+) create mode 100644 core/src/main/java/org/teavm/runtime/LaxMalloc.java diff --git a/core/src/main/java/org/teavm/runtime/LaxMalloc.java b/core/src/main/java/org/teavm/runtime/LaxMalloc.java new file mode 100644 index 000000000..cb5b94e34 --- /dev/null +++ b/core/src/main/java/org/teavm/runtime/LaxMalloc.java @@ -0,0 +1,285 @@ +/* + * Copyright 2024 lax1dude. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.runtime; + +import org.teavm.interop.Address; +import org.teavm.interop.Unmanaged; + +/** + * Linear memory allocator for creating "direct buffers" in WASM GC

+ * + * DO NOT USE IN LEGACY WASM BACKEND!!! Make a regular byte array, and use Address.ofData()

+ * + * Similar to dlmalloc and emmalloc (emscripten's malloc)

+ * + * bad things will happen if you free an address that was never allocated + * + * @author lax1dude + */ +@Unmanaged +public final class LaxMalloc { + + private LaxMalloc() { + } + + private static final int SIZEOF_PTR = 4; + private static final int SIZEOF_PTR_SH = 2; + + private static final int MIN_ALLOC_SIZE = 8; + + private static final int ADDR_HEAP_LIMIT = 4; // Address where we store the current heap limit (32 bit int) + private static final int ADDR_HEAP_BUCKETS_FREE_MASK = 8; // Address where we store the bitmask of free chunk lists (64 bit int) + private static final int ADDR_HEAP_BUCKETS_START = 16; // Address to the list of 64 pointers to the beginnings of the 64 buckets + private static final int ADDR_HEAP_DATA_START = 272; // Beginning of the first chunk of the heap + + /** + * malloc implementation + */ + public static Address laxMalloc(int sizeBytes) { + return laxAlloc(sizeBytes, false); + } + + /** + * calloc implementation (zeroed malloc) + */ + public static Address laxCalloc(int sizeBytes) { + return laxAlloc(sizeBytes, true); + } + + private static Address laxAlloc(int sizeBytes, boolean cleared) { + if(sizeBytes == 0) { + // Produce a null pointer if 0 size if requested + return Address.fromInt(0); + } + + // Allocation must be large enough to hold the two list pointers when the chunk becomes free again + if(sizeBytes < MIN_ALLOC_SIZE) { + sizeBytes = MIN_ALLOC_SIZE; + } + + // always between 0-63 + int bucket = getListBucket(sizeBytes); + + long bucketMask = Address.fromInt(ADDR_HEAP_BUCKETS_FREE_MASK).getLong(); + + // test the bucket mask if the bucket has any free chunks + if((bucketMask & (1L << bucket)) == 0l) { + // no more free chunks, let us first check if there is are any + // chunks in the larger buckets we can split + long largerBucketsMask = (bucketMask & (0xFFFFFFFFFFFFFFFFL << (bucket + 1))); + if(largerBucketsMask != 0l) { + // at least one larger chunk exists + int largerBucket = Long.numberOfTrailingZeros(largerBucketsMask); + Address bucketStartAddr = Address.fromInt(ADDR_HEAP_BUCKETS_START).add(largerBucket << SIZEOF_PTR_SH); + Address chunkPtr = bucketStartAddr.getAddress(); + //TODO: remove chunk from old bucket + int chunkSize = readChunkSize(chunkPtr); + int chunkOtherHalfNewSize = chunkSize - sizeBytes; + //TODO + //TODO + //TODO + + return null; //TODO + }else { + // No larger chunks already exist that we can split, + // time to sbrk + int sizePlusInts = sizeBytes + 8; // size + 2 ints + Address newChunk = growHeap(sizePlusInts); + + // Out of memory + if(newChunk.toInt() == 0) { + return Address.fromInt(0); //TODO + } + + // provision the new chunk + newChunk.putInt(sizePlusInts | 0x80000000); // size + in use flag + newChunk.add(sizeBytes + 4).putInt(sizePlusInts); // size integer at the end + + // return the chunk, +4 bytes to skip size int + // we don't need to clear it because its new + return newChunk.add(4); + } + }else { + // At least one free chunk is available, get it from the bucket + Address bucketStartAddr = Address.fromInt(ADDR_HEAP_BUCKETS_START).add(bucket << SIZEOF_PTR_SH); + Address chunkPtr = bucketStartAddr.getAddress(); + + Address nextStart = readChunkNextFreeAddr(chunkPtr); + if(nextStart.toInt() != 0) { + // there is another free chunk in the bucket that comes after this chunk, + // make the next free chunk in the list the first free chunk + bucketStartAddr.putAddress(nextStart); + writeChunkPrevFreeAddr(nextStart, Address.fromInt(0)); + }else { + // there are no remaining free chunks in the bucket + // clear the bit in the bucket bitmask + bucketMask = (bucketMask ^ (1L << bucket)); + Address.fromInt(ADDR_HEAP_BUCKETS_FREE_MASK).putLong(bucketMask); + + // set bucket start chunk pointer to null + bucketStartAddr.putAddress(Address.fromInt(0)); + } + + // mark the chunk in use + setChunkInUse(chunkPtr, true); + + // return the chunk we just removed from the list, +4 bytes to skip size + Address ret = chunkPtr.add(4); + + // clear if requested + if(cleared) { + Allocator.fillZero(ret, sizeBytes); + } + + return ret; + } + } + + /** + * free implementation

+ * + * bad things will happen if you free an address that was never allocated + */ + public static void laxFree(Address address) { + if(address.toInt() == 0) { + return; + } + + // chunk actually starts 4 bytes before + Address chunkPtr = address.add(-4); + + // set the chunk no longer in use + setChunkInUse(chunkPtr, false); + + // bring the size of the chunk into the stack + int chunkSize = chunkPtr.getInt(); + + if(Address.fromInt(ADDR_HEAP_DATA_START).isLessThan(chunkPtr)) { + // check if we can merge with the previous chunk, and move it to another bucket + Address prevChunkPtr = chunkPtr.add(-(chunkPtr.add(-4).getInt())); + int prevChunkSize = readChunkSizeStatus(prevChunkPtr); + if((prevChunkSize & 0x80000000) == 0) { + // previous chunk is not in use, merge! + + //TODO + } + } + + Address nextChunkPtr = chunkPtr.add(chunkSize); + if(Address.fromInt(ADDR_HEAP_LIMIT).getAddress().isLessThan(nextChunkPtr)) { + // check if we can merge with the next chunk as well + int nextChunkSize = readChunkSizeStatus(nextChunkPtr); + if((nextChunkSize & 0x80000000) == 0) { + // next chunk is not in use, merge! + + //TODO + } + } + + int bucket = getListBucket(chunkSize - 8); // size - 2 ints + + long bucketMask = Address.fromInt(ADDR_HEAP_BUCKETS_FREE_MASK).getLong(); + Address bucketStartAddr = Address.fromInt(ADDR_HEAP_BUCKETS_START).add(bucket << SIZEOF_PTR_SH); + + // test the bucket mask if the bucket is empty + if((bucketMask & (1L << bucket)) == 0l) { + // bucket is empty, add the free chunk to the list + bucketStartAddr.putAddress(chunkPtr); + writeChunkPrevFreeAddr(chunkPtr, Address.fromInt(0)); + writeChunkNextFreeAddr(chunkPtr, Address.fromInt(0)); + // set the free bit in bucket mask + bucketMask |= (1L << bucket); + Address.fromInt(ADDR_HEAP_BUCKETS_FREE_MASK).putLong(bucketMask); + }else { + // bucket is not empty, append to the bucket's existing free chunks list + Address otherBucketStart = bucketStartAddr.getAddress(); + writeChunkPrevFreeAddr(otherBucketStart, chunkPtr); + writeChunkNextFreeAddr(chunkPtr, otherBucketStart); + writeChunkPrevFreeAddr(chunkPtr, Address.fromInt(0)); + bucketStartAddr.putAddress(chunkPtr); + } + + } + + private static final int NUM_FREE_BUCKETS = 64; + + /** + * https://github.com/emscripten-core/emscripten/blob/16a0bf174cb85f88b6d9dcc8ee7fbca59390185b/system/lib/emmalloc.c#L241 + * (MIT License) + */ + private static int getListBucket(int allocSize) { + if (allocSize < 128) + return (allocSize >> 3) - 1; + + int clz = Integer.numberOfLeadingZeros(allocSize); + int bucketIndex = (clz > 19) ? 110 - (clz << 2) + ((allocSize >> (29 - clz)) ^ 4) + : min(71 - (clz << 1) + ((allocSize >> (30 - clz)) ^ 2), NUM_FREE_BUCKETS - 1); + + return bucketIndex; + } + + /** + * This is our sbrk + */ + private static Address growHeap(int amount) { + Address heapLimit = Address.fromInt(ADDR_HEAP_LIMIT).getAddress(); + Address.fromInt(ADDR_HEAP_LIMIT).putAddress(heapLimit.add(amount)); + //TODO: expand WebAssembly Memory + return heapLimit; + } + + private static int readChunkSizeStatus(Address chunkAddr) { + return chunkAddr.getInt(); + } + + private static int readChunkSize(Address chunkAddr) { + return chunkAddr.getInt() & 0x7FFFFFFF; + } + + private static boolean readChunkInUse(Address chunkAddr) { + return (chunkAddr.getInt() & 0x80000000) != 0; + } + + private static void writeChunkSizeStatus(Address chunkAddr, int sizeStatus) { + chunkAddr.putInt(sizeStatus); + } + + private static void setChunkInUse(Address chunkAddr, boolean inUse) { + int i = chunkAddr.getInt(); + chunkAddr.putInt(inUse ? (i | 0x80000000) : (i & 0x7FFFFFFF)); + } + + private static Address readChunkPrevFreeAddr(Address chunkAddr) { + return chunkAddr.add(4).getAddress(); + } + + private static void writeChunkPrevFreeAddr(Address chunkAddr, Address prevFree) { + chunkAddr.add(4).putAddress(prevFree); + } + + private static Address readChunkNextFreeAddr(Address chunkAddr) { + return chunkAddr.add(4 + (1 << SIZEOF_PTR_SH)).getAddress(); + } + + private static void writeChunkNextFreeAddr(Address chunkAddr, Address nextFree) { + chunkAddr.add(4 + (1 << SIZEOF_PTR_SH)).putAddress(nextFree); + } + + private static int min(int a, int b) { + return a < b ? a : b; + } + +}