Wasm: support file IO and random generator in WASI

This commit is contained in:
Alexey Andreev 2022-11-08 19:44:56 +01:00
parent e428e2ac7a
commit 224810d0ab
11 changed files with 141 additions and 62 deletions

View File

@ -18,6 +18,7 @@ package org.teavm.classlib.java.util;
import java.util.function.DoublePredicate; import java.util.function.DoublePredicate;
import java.util.function.IntPredicate; import java.util.function.IntPredicate;
import java.util.function.LongPredicate; import java.util.function.LongPredicate;
import org.teavm.backend.wasm.runtime.WasmSupport;
import org.teavm.classlib.PlatformDetector; import org.teavm.classlib.PlatformDetector;
import org.teavm.classlib.java.io.TSerializable; import org.teavm.classlib.java.io.TSerializable;
import org.teavm.classlib.java.lang.TMath; import org.teavm.classlib.java.lang.TMath;
@ -144,6 +145,8 @@ public class TRandom extends TObject implements TSerializable {
public double nextDouble() { public double nextDouble() {
if (PlatformDetector.isC()) { if (PlatformDetector.isC()) {
return crand(); return crand();
} else if (PlatformDetector.isWebAssembly()) {
return WasmSupport.random();
} else { } else {
return random(); return random();
} }

View File

@ -26,6 +26,7 @@ public final class WasmHeap {
public static final int PAGE_SIZE = 65536; public static final int PAGE_SIZE = 65536;
public static final int DEFAULT_STACK_SIZE = PAGE_SIZE * 4; public static final int DEFAULT_STACK_SIZE = PAGE_SIZE * 4;
public static final int DEFAULT_REGION_SIZE = 1024; public static final int DEFAULT_REGION_SIZE = 1024;
public static final int DEFAULT_BUFFER_SIZE = 512;
public static int minHeapSize; public static int minHeapSize;
public static int maxHeapSize; public static int maxHeapSize;
@ -42,6 +43,8 @@ public final class WasmHeap {
public static Address stackAddress; public static Address stackAddress;
public static Address stack; public static Address stack;
public static int stackSize; public static int stackSize;
public static Address buffer;
public static int bufferSize;
private WasmHeap() { private WasmHeap() {
} }
@ -64,10 +67,13 @@ public final class WasmHeap {
WasmSupport.initHeapTrace(maxHeap); WasmSupport.initHeapTrace(maxHeap);
} }
public static void initHeap(Address start, int minHeap, int maxHeap, int stackSize) { public static void initHeap(Address start, int minHeap, int maxHeap, int stackSize, int bufferSize) {
initHeapTrace(maxHeap); initHeapTrace(maxHeap);
stackAddress = start; buffer = start;
stack = start; buffer = WasmRuntime.align(start, 16);
WasmHeap.bufferSize = bufferSize;
stack = WasmRuntime.align(buffer.add(bufferSize), 16);
stackAddress = stack;
heapAddress = WasmRuntime.align(stackAddress.add(stackSize), 16); heapAddress = WasmRuntime.align(stackAddress.add(stackSize), 16);
memoryLimit = WasmRuntime.align(start, PAGE_SIZE); memoryLimit = WasmRuntime.align(start, PAGE_SIZE);
minHeapSize = minHeap; minHeapSize = minHeap;

View File

@ -175,7 +175,7 @@ import org.teavm.vm.spi.TeaVMHostExtension;
public class WasmTarget implements TeaVMTarget, TeaVMWasmHost { public class WasmTarget implements TeaVMTarget, TeaVMWasmHost {
private static final MethodReference INIT_HEAP_REF = new MethodReference(WasmHeap.class, "initHeap", private static final MethodReference INIT_HEAP_REF = new MethodReference(WasmHeap.class, "initHeap",
Address.class, int.class, int.class, int.class, void.class); Address.class, int.class, int.class, int.class, int.class, void.class);
private static final MethodReference RESIZE_HEAP_REF = new MethodReference(WasmHeap.class, "resizeHeap", private static final MethodReference RESIZE_HEAP_REF = new MethodReference(WasmHeap.class, "resizeHeap",
int.class, void.class); int.class, void.class);
private static final Set<MethodReference> VIRTUAL_METHODS = new HashSet<>(Arrays.asList( private static final Set<MethodReference> VIRTUAL_METHODS = new HashSet<>(Arrays.asList(
@ -621,7 +621,8 @@ public class WasmTarget implements TeaVMTarget, TeaVMWasmHost {
initFunction.getBody().add(new WasmCall(names.forMethod(INIT_HEAP_REF), initFunction.getBody().add(new WasmCall(names.forMethod(INIT_HEAP_REF),
new WasmInt32Constant(heapAddress), new WasmInt32Constant(minHeapSize), new WasmInt32Constant(heapAddress), new WasmInt32Constant(minHeapSize),
new WasmInt32Constant(maxHeapSize), new WasmInt32Constant(WasmHeap.DEFAULT_STACK_SIZE))); new WasmInt32Constant(maxHeapSize), new WasmInt32Constant(WasmHeap.DEFAULT_STACK_SIZE),
new WasmInt32Constant(WasmHeap.DEFAULT_BUFFER_SIZE)));
for (Class<?> javaCls : new Class<?>[] { GC.class }) { for (Class<?> javaCls : new Class<?>[] { GC.class }) {
ClassReader cls = classes.get(javaCls.getName()); ClassReader cls = classes.get(javaCls.getName());
@ -949,6 +950,8 @@ public class WasmTarget implements TeaVMTarget, TeaVMWasmHost {
int newRegionsCount = WasmHeap.calculateRegionsCount(maxHeapSize, WasmHeap.DEFAULT_REGION_SIZE); int newRegionsCount = WasmHeap.calculateRegionsCount(maxHeapSize, WasmHeap.DEFAULT_REGION_SIZE);
int newRegionsSize = WasmHeap.calculateRegionsSize(newRegionsCount); int newRegionsSize = WasmHeap.calculateRegionsSize(newRegionsCount);
address = WasmRuntime.align(address, 16);
address = WasmRuntime.align(address + WasmHeap.DEFAULT_BUFFER_SIZE, 16);
address = WasmRuntime.align(address + WasmHeap.DEFAULT_STACK_SIZE, 16); address = WasmRuntime.align(address + WasmHeap.DEFAULT_STACK_SIZE, 16);
address = WasmRuntime.align(address + maxHeapSize, 16); address = WasmRuntime.align(address + maxHeapSize, 16);
address = WasmRuntime.align(address + newRegionsSize, 16); address = WasmRuntime.align(address + newRegionsSize, 16);

View File

@ -15,19 +15,18 @@
*/ */
package org.teavm.backend.wasm.runtime; package org.teavm.backend.wasm.runtime;
import org.teavm.backend.wasm.WasmHeap;
import org.teavm.interop.Address; import org.teavm.interop.Address;
public final class WasiBuffer { public final class WasiBuffer {
private static byte[] argBuffer = new byte[256];
private WasiBuffer() { private WasiBuffer() {
} }
public static int getBufferSize() { public static int getBufferSize() {
return argBuffer.length; return WasmHeap.bufferSize;
} }
public static Address getBuffer() { public static Address getBuffer() {
return Address.ofData(argBuffer); return WasmHeap.buffer;
} }
} }

View File

@ -15,8 +15,10 @@
*/ */
package org.teavm.backend.wasm.runtime; package org.teavm.backend.wasm.runtime;
import static org.teavm.backend.wasm.wasi.Wasi.ERRNO_SUCCESS;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import org.teavm.backend.wasm.wasi.IOVec; import org.teavm.backend.wasm.wasi.IOVec;
import org.teavm.backend.wasm.wasi.IntResult;
import org.teavm.backend.wasm.wasi.LongResult; import org.teavm.backend.wasm.wasi.LongResult;
import org.teavm.backend.wasm.wasi.SizeResult; import org.teavm.backend.wasm.wasi.SizeResult;
import org.teavm.backend.wasm.wasi.Wasi; import org.teavm.backend.wasm.wasi.Wasi;
@ -26,6 +28,9 @@ import org.teavm.interop.Unmanaged;
@Unmanaged @Unmanaged
public class WasiSupport { public class WasiSupport {
private static long nextRandom;
private static boolean randomInitialized;
private WasiSupport() { private WasiSupport() {
} }
@ -43,7 +48,7 @@ public class WasiSupport {
public static void putChars(int fd, Address address, int count) { public static void putChars(int fd, Address address, int count) {
Address argsAddress = WasiBuffer.getBuffer(); Address argsAddress = WasiBuffer.getBuffer();
IOVec ioVec = argsAddress.toStructure(); IOVec ioVec = argsAddress.toStructure();
SizeResult result = argsAddress.add(Structure.sizeOf(IOVec.class)).toStructure(); SizeResult result = Address.align(argsAddress.add(Structure.sizeOf(IOVec.class)), 16).toStructure();
ioVec.buffer = address; ioVec.buffer = address;
ioVec.bufferLength = count; ioVec.bufferLength = count;
Wasi.fdWrite(fd, ioVec, 1, result); Wasi.fdWrite(fd, ioVec, 1, result);
@ -51,8 +56,7 @@ public class WasiSupport {
@Unmanaged @Unmanaged
public static long currentTimeMillis() { public static long currentTimeMillis() {
Address argsAddress = WasiBuffer.getBuffer(); LongResult result = WasiBuffer.getBuffer().toStructure();
LongResult result = argsAddress.toStructure();
Wasi.clockTimeGet(Wasi.CLOCKID_REALTIME, 10, result); Wasi.clockTimeGet(Wasi.CLOCKID_REALTIME, 10, result);
return result.value; return result.value;
} }
@ -103,21 +107,22 @@ public class WasiSupport {
public static String[] getArgs() { public static String[] getArgs() {
Address buffer = WasiBuffer.getBuffer(); Address buffer = WasiBuffer.getBuffer();
SizeResult sizesReceiver = buffer.toStructure(); IntResult sizesReceiver = buffer.toStructure();
SizeResult bufferSizeReceiver = buffer.add(Structure.sizeOf(SizeResult.class)).toStructure(); IntResult bufferSizeReceiver = Address.align(buffer.add(Structure.sizeOf(IntResult.class)), 16)
.toStructure();
short errno = Wasi.argsSizesGet(sizesReceiver, bufferSizeReceiver); short errno = Wasi.argsSizesGet(sizesReceiver, bufferSizeReceiver);
if (errno != Wasi.ERRNO_SUCCESS) { if (errno != ERRNO_SUCCESS) {
throw new RuntimeException("Could not get command line arguments"); throw new RuntimeException("Could not get command line arguments");
} }
int argvSize = sizesReceiver.value; int argvSize = (int) sizesReceiver.value;
int argvBufSize = bufferSizeReceiver.value; int argvBufSize = (int) bufferSizeReceiver.value;
int[] argvOffsets = new int[argvSize]; int[] argvOffsets = new int[argvSize];
byte[] argvBuffer = new byte[argvBufSize]; byte[] argvBuffer = new byte[argvBufSize];
errno = Wasi.argsGet(Address.ofData(argvOffsets), Address.ofData(argvBuffer)); errno = Wasi.argsGet(Address.ofData(argvOffsets), Address.ofData(argvBuffer));
if (errno != Wasi.ERRNO_SUCCESS) { if (errno != ERRNO_SUCCESS) {
throw new RuntimeException("Could not get command line arguments"); throw new RuntimeException("Could not get command line arguments");
} }
@ -130,4 +135,23 @@ public class WasiSupport {
return args; return args;
} }
public static double random() {
return (((long) nextRandom(26) << 27) + nextRandom(27)) / (double) (1L << 53);
}
private static int nextRandom(int bits) {
if (!randomInitialized) {
short errno = Wasi.randomGet(WasiBuffer.getBuffer(), 8);
if (errno != ERRNO_SUCCESS) {
throw new RuntimeException("random_get: " + errno);
}
nextRandom = WasiBuffer.getBuffer().getLong();
}
nextRandom = ((nextRandom * 0x5DEECE66DL) + 0xBL) & ((1L << 48) - 1);
return (int) (nextRandom >>> (48 - bits));
}
} }

View File

@ -53,4 +53,7 @@ public class WasmSupport {
public static String[] getArgs() { public static String[] getArgs() {
return new String[0]; return new String[0];
} }
@Import(module = "teavmMath", name = "random")
public static native double random();
} }

View File

@ -80,7 +80,7 @@ public class WasiVirtualFile implements VirtualFile {
if (baseFd < 0) { if (baseFd < 0) {
return null; return null;
} }
Filestat filestat = Address.align(WasiBuffer.getBuffer(), 8).toStructure(); Filestat filestat = WasiBuffer.getBuffer().toStructure();
int errno; int errno;
if (path != null) { if (path != null) {
byte[] bytes = path.getBytes(StandardCharsets.UTF_8); byte[] bytes = path.getBytes(StandardCharsets.UTF_8);
@ -207,7 +207,7 @@ public class WasiVirtualFile implements VirtualFile {
fdflags |= FDFLAGS_APPEND; fdflags |= FDFLAGS_APPEND;
} }
int fd = open((short) 0, rights, fdflags); int fd = open((short) 0, rights, fdflags);
return fd >= 0 ? new WasiVirtualFileAccessor(fs, fd) : null; return fd >= 0 ? new WasiVirtualFileAccessor(this, fd) : null;
} }
@Override @Override

View File

@ -15,88 +15,88 @@
*/ */
package org.teavm.backend.wasm.runtime.fs; package org.teavm.backend.wasm.runtime.fs;
import static org.teavm.backend.wasm.wasi.Wasi.ERRNO_SUCCESS;
import static org.teavm.backend.wasm.wasi.Wasi.WHENCE_CURRENT;
import static org.teavm.backend.wasm.wasi.Wasi.WHENCE_START;
import java.io.IOException; import java.io.IOException;
import org.teavm.backend.wasm.runtime.WasiBuffer; import org.teavm.backend.wasm.runtime.WasiBuffer;
import org.teavm.backend.wasm.wasi.IOVec;
import org.teavm.backend.wasm.wasi.SizeResult;
import org.teavm.backend.wasm.wasi.Wasi;
import org.teavm.interop.Address; import org.teavm.interop.Address;
import org.teavm.interop.Structure;
import org.teavm.runtime.fs.VirtualFileAccessor; import org.teavm.runtime.fs.VirtualFileAccessor;
public class WasiVirtualFileAccessor implements VirtualFileAccessor { public class WasiVirtualFileAccessor implements VirtualFileAccessor {
// Enough room for an I32 plus padding for alignment: private WasiVirtualFile file;
private static final byte[] EIGHT_BYTE_BUFFER = new byte[8];
// Enough room for an I64 plus padding for alignment:
private static final byte[] SIXTEEN_BYTE_BUFFER = new byte[16];
private WasiFileSystem fs;
private int fd; private int fd;
public WasiVirtualFileAccessor(WasiFileSystem fs, int fd) { WasiVirtualFileAccessor(WasiVirtualFile file, int fd) {
this.fs = fs; this.file = file;
this.fd = fd; this.fd = fd;
} }
@Override @Override
public int read(byte[] buffer, int offset, int length) throws IOException { public int read(byte[] buffer, int offset, int length) throws IOException {
byte[] vecBuffer = SIXTEEN_BYTE_BUFFER; Address buf = WasiBuffer.getBuffer();
Address vec = WasiBuffer.getBuffer(); IOVec vec = buf.toStructure();
vec.putInt(Address.ofData(buffer).add(offset).toInt()); vec.buffer = Address.ofData(buffer).add(offset);
vec.add(4).putInt(length); vec.bufferLength = length;
byte[] sizeBuffer = EIGHT_BYTE_BUFFER;
Address size = Address.align(Address.ofData(sizeBuffer), 4); SizeResult sizeResult = Address.align(buf.add(Structure.sizeOf(IOVec.class)), 16).toStructure();
short errno = Wasi.fdRead(fd, vec, 1, size); short errno = Wasi.fdRead(fd, vec, 1, sizeResult);
if (errno == ERRNO_SUCCESS) { if (errno == ERRNO_SUCCESS) {
return size.getInt(); return (int) sizeResult.value;
} else { } else {
throw new IOException(errnoMessage("fd_read", errno)); throw new IOException("fd_read: " + errno);
} }
} }
@Override @Override
public void write(byte[] buffer, int offset, int length) throws IOException { public void write(byte[] buffer, int offset, int length) throws IOException {
byte[] vecBuffer = SIXTEEN_BYTE_BUFFER; Address buf = WasiBuffer.getBuffer();
Address vec = Address.align(Address.ofData(vecBuffer), 4); IOVec vec = buf.toStructure();
byte[] sizeBuffer = EIGHT_BYTE_BUFFER; SizeResult sizeResult = Address.align(buf.add(Structure.sizeOf(IOVec.class)), 16).toStructure();
Address size = Address.align(Address.ofData(sizeBuffer), 4);
int index = 0;
while (true) { while (true) {
vec.putInt(Address.ofData(buffer).add(offset + index).toInt()); vec.buffer = Address.ofData(buffer).add(offset);
vec.add(4).putInt(length - index); vec.bufferLength = length;
short errno = Wasi.fdWrite(fd, vec, 1, size); short errno = Wasi.fdWrite(fd, vec, 1, sizeResult);
if (errno == ERRNO_SUCCESS) { if (errno == ERRNO_SUCCESS) {
index += size.getInt(); int size = (int) sizeResult.value;
if (index >= length) { offset += size;
length -= size;
if (length <= 0) {
return; return;
} }
} else { } else {
throw new IOException(errnoMessage("fd_write", errno)); throw new IOException("fd_write: " + errno);
} }
} }
} }
@Override @Override
public int tell() throws IOException { public int tell() throws IOException {
byte[] filesizeBuffer = SIXTEEN_BYTE_BUFFER; SizeResult filesize = WasiBuffer.getBuffer().toStructure();
Address filesize = Address.align(Address.ofData(filesizeBuffer), 8);
short errno = Wasi.fdTell(fd, filesize); short errno = Wasi.fdTell(fd, filesize);
if (errno == ERRNO_SUCCESS) { if (errno == ERRNO_SUCCESS) {
return (int) filesize.getLong(); return (int) filesize.value;
} else { } else {
throw new IOException(errnoMessage("fd_tell", errno)); throw new IOException("fd_tell: " + errno);
} }
} }
private long seek(long offset, byte whence) throws IOException { private long seek(long offset, byte whence) throws IOException {
byte[] filesizeBuffer = SIXTEEN_BYTE_BUFFER; SizeResult filesize = WasiBuffer.getBuffer().toStructure();
Address filesize = Address.align(Address.ofData(filesizeBuffer), 8);
short errno = Wasi.fdSeek(fd, offset, whence, filesize); short errno = Wasi.fdSeek(fd, offset, whence, filesize);
if (errno == ERRNO_SUCCESS) { if (errno == ERRNO_SUCCESS) {
return filesize.getLong(); return filesize.value;
} else { } else {
throw new IOException(errnoMessage("fd_seek", errno)); throw new IOException("fd_seek: " + errno);
} }
} }
@ -112,7 +112,7 @@ public class WasiVirtualFileAccessor implements VirtualFileAccessor {
@Override @Override
public int size() throws IOException { public int size() throws IOException {
return (int) new WasiVirtualFile(fd, null).stat().filesize; return (int) file.stat().filesize;
} }
@Override @Override
@ -120,7 +120,7 @@ public class WasiVirtualFileAccessor implements VirtualFileAccessor {
short errno = Wasi.fdFilestatSetSize(fd, size); short errno = Wasi.fdFilestatSetSize(fd, size);
if (errno != ERRNO_SUCCESS) { if (errno != ERRNO_SUCCESS) {
throw new IOException(errnoMessage("fd_filestat_set_size", errno)); throw new IOException("fd_filestat_set_size" + errno);
} }
} }
@ -133,7 +133,7 @@ public class WasiVirtualFileAccessor implements VirtualFileAccessor {
short errno = Wasi.fdClose(fd); short errno = Wasi.fdClose(fd);
if (errno != ERRNO_SUCCESS) { if (errno != ERRNO_SUCCESS) {
throw new IOException(errnoMessage("fd_close", errno)); throw new IOException("fd_close: " + errno);
} }
} }
} }
@ -143,7 +143,7 @@ public class WasiVirtualFileAccessor implements VirtualFileAccessor {
short errno = Wasi.fdSync(fd); short errno = Wasi.fdSync(fd);
if (errno != ERRNO_SUCCESS) { if (errno != ERRNO_SUCCESS) {
throw new IOException(errnoMessage("fd_sync", errno)); throw new IOException("fd_sync: " + errno);
} }
} }
} }

View File

@ -0,0 +1,22 @@
/*
* Copyright 2022 Alexey Andreev.
*
* 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.backend.wasm.wasi;
import org.teavm.interop.Structure;
public class IntResult extends Structure {
public int value;
}

View File

@ -18,5 +18,5 @@ package org.teavm.backend.wasm.wasi;
import org.teavm.interop.Structure; import org.teavm.interop.Structure;
public class SizeResult extends Structure { public class SizeResult extends Structure {
public int value; public long value;
} }

View File

@ -62,14 +62,23 @@ public final class Wasi {
public static native short clockTimeGet(int clockId, long precision, LongResult result); public static native short clockTimeGet(int clockId, long precision, LongResult result);
@Import(name = "args_sizes_get", module = "wasi_snapshot_preview1") @Import(name = "args_sizes_get", module = "wasi_snapshot_preview1")
public static native short argsSizesGet(SizeResult argvSize, SizeResult argvBufSize); public static native short argsSizesGet(IntResult argvSize, IntResult argvBufSize);
@Import(name = "args_get", module = "wasi_snapshot_preview1") @Import(name = "args_get", module = "wasi_snapshot_preview1")
public static native short argsGet(Address argv, Address argvBuf); public static native short argsGet(Address argv, Address argvBuf);
@Import(name = "fd_read", module = "wasi_snapshot_preview1")
public static native short fdRead(int fd, IOVec vecArray, int vecArrayLength, SizeResult size);
@Import(name = "fd_write", module = "wasi_snapshot_preview1") @Import(name = "fd_write", module = "wasi_snapshot_preview1")
public static native short fdWrite(int fd, IOVec vectors, int vectorsCont, SizeResult result); public static native short fdWrite(int fd, IOVec vectors, int vectorsCont, SizeResult result);
@Import(name = "fd_tell", module = "wasi_snapshot_preview1")
public static native short fdTell(int fd, SizeResult size);
@Import(name = "fd_seek", module = "wasi_snapshot_preview1")
public static native short fdSeek(int fd, long offset, byte whence, SizeResult size);
@Import(name = "fd_prestat_get", module = "wasi_snapshot_preview1") @Import(name = "fd_prestat_get", module = "wasi_snapshot_preview1")
public static native short fdPrestatGet(int fd, Prestat prestat); public static native short fdPrestatGet(int fd, Prestat prestat);
@ -109,4 +118,14 @@ public final class Wasi {
@Import(name = "path_filestat_set_times", module = "wasi_snapshot_preview1") @Import(name = "path_filestat_set_times", module = "wasi_snapshot_preview1")
public static native short pathFilestatSetTimes(int fd, int lookupFlags, Address path, int pathLength, public static native short pathFilestatSetTimes(int fd, int lookupFlags, Address path, int pathLength,
long atime, long mtime, short fstflags); long atime, long mtime, short fstflags);
@Import(name = "fd_filestat_set_size", module = "wasi_snapshot_preview1")
public static native short fdFilestatSetSize(int fd, long size);
@Import(name = "fd_sync", module = "wasi_snapshot_preview1")
public static native short fdSync(int fd);
@Import(name = "random_get", module = "wasi_snapshot_preview1")
public static native short randomGet(Address buffer, int bufferLength);
} }