From e428e2ac7a3d6d1dba652d1cb28e1586cca8d645 Mon Sep 17 00:00:00 2001 From: Alexey Andreev Date: Mon, 7 Nov 2022 17:18:32 +0100 Subject: [PATCH] Wasm: implement file IO in WASI --- .../backend/c/intrinsic/AddressIntrinsic.java | 14 + .../wasm/intrinsics/AddressIntrinsic.java | 3 + .../wasm/runtime/fs/WasiFileSystem.java | 159 ++++++++ .../wasm/runtime/fs/WasiVirtualFile.java | 368 ++++++++++++++++++ .../runtime/fs/WasiVirtualFileAccessor.java | 149 +++++++ .../org/teavm/backend/wasm/wasi/Dirent.java | 25 ++ .../org/teavm/backend/wasm/wasi/FdResult.java | 22 ++ .../org/teavm/backend/wasm/wasi/Filestat.java | 29 ++ .../org/teavm/backend/wasm/wasi/Prestat.java | 22 ++ .../teavm/backend/wasm/wasi/PrestatDir.java | 20 + .../org/teavm/backend/wasm/wasi/Wasi.java | 79 +++- .../main/java/org/teavm/interop/Address.java | 2 + 12 files changed, 889 insertions(+), 3 deletions(-) create mode 100644 core/src/main/java/org/teavm/backend/wasm/runtime/fs/WasiFileSystem.java create mode 100644 core/src/main/java/org/teavm/backend/wasm/runtime/fs/WasiVirtualFile.java create mode 100644 core/src/main/java/org/teavm/backend/wasm/runtime/fs/WasiVirtualFileAccessor.java create mode 100644 core/src/main/java/org/teavm/backend/wasm/wasi/Dirent.java create mode 100644 core/src/main/java/org/teavm/backend/wasm/wasi/FdResult.java create mode 100644 core/src/main/java/org/teavm/backend/wasm/wasi/Filestat.java create mode 100644 core/src/main/java/org/teavm/backend/wasm/wasi/Prestat.java create mode 100644 core/src/main/java/org/teavm/backend/wasm/wasi/PrestatDir.java diff --git a/core/src/main/java/org/teavm/backend/c/intrinsic/AddressIntrinsic.java b/core/src/main/java/org/teavm/backend/c/intrinsic/AddressIntrinsic.java index 1d6cffe48..95f58d42d 100644 --- a/core/src/main/java/org/teavm/backend/c/intrinsic/AddressIntrinsic.java +++ b/core/src/main/java/org/teavm/backend/c/intrinsic/AddressIntrinsic.java @@ -15,7 +15,9 @@ */ package org.teavm.backend.c.intrinsic; +import org.teavm.ast.Expr; import org.teavm.ast.InvocationExpr; +import org.teavm.ast.VariableExpr; import org.teavm.backend.c.generate.CodeGeneratorUtil; import org.teavm.backend.c.util.ConstantUtil; import org.teavm.interop.Address; @@ -62,6 +64,8 @@ public class AddressIntrinsic implements Intrinsic { case "sizeOf": case "ofData": + + case "pin": return true; default: return false; @@ -214,6 +218,16 @@ public class AddressIntrinsic implements Intrinsic { + sizeOf(type.getItemType()) + "))"); break; } + + case "pin": { + context.writer().print("/* PIN "); + Expr arg = invocation.getArguments().get(0); + if (arg instanceof VariableExpr) { + context.writer().print(((VariableExpr) arg).getIndex() + " "); + } + context.writer().print("*/"); + break; + } } } diff --git a/core/src/main/java/org/teavm/backend/wasm/intrinsics/AddressIntrinsic.java b/core/src/main/java/org/teavm/backend/wasm/intrinsics/AddressIntrinsic.java index 3520cd78b..474d700f1 100644 --- a/core/src/main/java/org/teavm/backend/wasm/intrinsics/AddressIntrinsic.java +++ b/core/src/main/java/org/teavm/backend/wasm/intrinsics/AddressIntrinsic.java @@ -23,6 +23,7 @@ import org.teavm.backend.wasm.generate.WasmClassGenerator; import org.teavm.backend.wasm.model.WasmType; import org.teavm.backend.wasm.model.expression.WasmCall; import org.teavm.backend.wasm.model.expression.WasmConversion; +import org.teavm.backend.wasm.model.expression.WasmDrop; import org.teavm.backend.wasm.model.expression.WasmExpression; import org.teavm.backend.wasm.model.expression.WasmInt32Constant; import org.teavm.backend.wasm.model.expression.WasmInt32Subtype; @@ -172,6 +173,8 @@ public class AddressIntrinsic implements WasmIntrinsic { return new WasmIntBinary(WasmIntType.INT32, WasmIntBinaryOperation.ADD, manager.generate(invocation.getArguments().get(0)), new WasmInt32Constant(start)); } + case "pin": + return new WasmDrop(new WasmInt32Constant(0)); default: throw new IllegalArgumentException(invocation.getMethod().toString()); } diff --git a/core/src/main/java/org/teavm/backend/wasm/runtime/fs/WasiFileSystem.java b/core/src/main/java/org/teavm/backend/wasm/runtime/fs/WasiFileSystem.java new file mode 100644 index 000000000..cc8b2ed76 --- /dev/null +++ b/core/src/main/java/org/teavm/backend/wasm/runtime/fs/WasiFileSystem.java @@ -0,0 +1,159 @@ +/* + * Copyright 2022 TeaVM Contributors. + * + * 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.runtime.fs; + +import static org.teavm.backend.wasm.wasi.Wasi.ERRNO_BADF; +import static org.teavm.backend.wasm.wasi.Wasi.ERRNO_SUCCESS; +import static org.teavm.backend.wasm.wasi.Wasi.PRESTAT_DIR; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.teavm.backend.wasm.wasi.Prestat; +import org.teavm.backend.wasm.wasi.PrestatDir; +import org.teavm.backend.wasm.wasi.Wasi; +import org.teavm.interop.Address; +import org.teavm.runtime.fs.VirtualFile; +import org.teavm.runtime.fs.VirtualFileSystem; + +public class WasiFileSystem implements VirtualFileSystem { + private List preopenedList; + final byte[] buffer = new byte[256]; + short errno; + String bestPreopenedPath; + int bestPreopenedId; + + @Override + public String getUserDir() { + return "/"; + } + + @Override + public VirtualFile getFile(String path) { + return new WasiVirtualFile(this, path); + } + + void findBestPreopened(String path) { + if (path.startsWith("/")) { + path = path.substring(1); + } + + if (path.endsWith("/")) { + path = path.substring(0, path.length() - 1); + } + + List list = getPreopenedList(); + int bestFd = -1; + int bestNameLength = -1; + for (Preopened preopened : list) { + if (path.equals(preopened.name)) { + bestFd = preopened.fd; + path = null; + break; + } + + if (preopened.name.length() > bestNameLength) { + int prefixLen = getPrefixLength(path, preopened.name); + if (prefixLen >= 0) { + bestFd = preopened.fd; + bestNameLength = prefixLen; + } + } + } + + if (bestFd == -1) { + path = null; + } else if (path != null) { + path = path.substring(bestNameLength); + } + + bestPreopenedPath = path; + bestPreopenedId = bestFd; + } + + private static int getPrefixLength(String name, String dir) { + if (dir.equals("/")) { + return 0; + } else { + return name.startsWith(dir) && name.length() > dir.length() && name.charAt(dir.length()) == '/' + ? dir.length() + 1 : -1; + } + } + + @Override + public boolean isWindows() { + return false; + } + + @Override + public String canonicalize(String path) { + return path; + } + + private List getPreopenedList() { + if (preopenedList == null) { + preopenedList = createPreopenedList(); + } + return preopenedList; + } + + private List createPreopenedList() { + byte[] buffer = this.buffer; + Prestat prestat = Address.align(Address.ofData(buffer), 4).toStructure(); + List list = new ArrayList<>(); + int fd = 3; // skip stdin, stdout, and stderr + while (true) { + short errno = Wasi.fdPrestatGet(fd, prestat); + if (errno == ERRNO_BADF) { + break; + } + if (errno != ERRNO_BADF) { + return Collections.emptyList(); + } + + if (prestat.kind == PRESTAT_DIR) { + PrestatDir prestatDir = (PrestatDir) prestat; + int length = prestatDir.nameLength; + byte[] bytes = length <= buffer.length ? buffer : new byte[length]; + errno = Wasi.fdPrestatDirName(fd, Address.ofData(bytes), length); + + if (errno == ERRNO_SUCCESS && length > 0) { + if (bytes[length - 1] == 0) { + length -= 1; + } + + list.add(new Preopened(fd, new String(bytes, 0, length, StandardCharsets.UTF_8))); + } else { + return Collections.emptyList(); + } + } + + fd += 1; + } + + return list; + } + + static class Preopened { + final int fd; + final String name; + + Preopened(int fd, String name) { + this.fd = fd; + this.name = name; + } + } +} \ No newline at end of file diff --git a/core/src/main/java/org/teavm/backend/wasm/runtime/fs/WasiVirtualFile.java b/core/src/main/java/org/teavm/backend/wasm/runtime/fs/WasiVirtualFile.java new file mode 100644 index 000000000..c5d370b28 --- /dev/null +++ b/core/src/main/java/org/teavm/backend/wasm/runtime/fs/WasiVirtualFile.java @@ -0,0 +1,368 @@ +/* + * Copyright 2022 TeaVM Contributors. + * + * 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.runtime.fs; + +import static org.teavm.backend.wasm.wasi.Wasi.DIRFLAGS_FOLLOW_SYMLINKS; +import static org.teavm.backend.wasm.wasi.Wasi.ERRNO_EXIST; +import static org.teavm.backend.wasm.wasi.Wasi.ERRNO_SUCCESS; +import static org.teavm.backend.wasm.wasi.Wasi.FDFLAGS_APPEND; +import static org.teavm.backend.wasm.wasi.Wasi.FILETYPE_DIRECTORY; +import static org.teavm.backend.wasm.wasi.Wasi.FILETYPE_REGULAR_FILE; +import static org.teavm.backend.wasm.wasi.Wasi.FSTFLAGS_MTIME; +import static org.teavm.backend.wasm.wasi.Wasi.OFLAGS_CREATE; +import static org.teavm.backend.wasm.wasi.Wasi.OFLAGS_DIRECTORY; +import static org.teavm.backend.wasm.wasi.Wasi.OFLAGS_EXCLUSIVE; +import static org.teavm.backend.wasm.wasi.Wasi.RIGHTS_FD_FILESTAT_GET; +import static org.teavm.backend.wasm.wasi.Wasi.RIGHTS_FD_FILESTAT_SET_SIZE; +import static org.teavm.backend.wasm.wasi.Wasi.RIGHTS_READ; +import static org.teavm.backend.wasm.wasi.Wasi.RIGHTS_SEEK; +import static org.teavm.backend.wasm.wasi.Wasi.RIGHTS_SYNC; +import static org.teavm.backend.wasm.wasi.Wasi.RIGHTS_TELL; +import static org.teavm.backend.wasm.wasi.Wasi.RIGHTS_WRITE; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import org.teavm.backend.wasm.runtime.WasiBuffer; +import org.teavm.backend.wasm.wasi.Dirent; +import org.teavm.backend.wasm.wasi.FdResult; +import org.teavm.backend.wasm.wasi.Filestat; +import org.teavm.backend.wasm.wasi.SizeResult; +import org.teavm.backend.wasm.wasi.Wasi; +import org.teavm.interop.Address; +import org.teavm.interop.Structure; +import org.teavm.runtime.fs.VirtualFile; +import org.teavm.runtime.fs.VirtualFileAccessor; + +public class WasiVirtualFile implements VirtualFile { + private final WasiFileSystem fs; + private final String fullPath; + private boolean initialized; + private int baseFd; + private String path; + + WasiVirtualFile(WasiFileSystem fs, String path) { + this.fs = fs; + fullPath = path; + } + + private void init() { + if (initialized) { + return; + } + initialized = true; + + fs.findBestPreopened(path); + path = fs.bestPreopenedPath; + baseFd = fs.bestPreopenedId; + fs.bestPreopenedPath = null; + } + + @Override + public String getName() { + return path.substring(path.lastIndexOf('/') + 1); + } + + Stat stat() { + init(); + if (baseFd < 0) { + return null; + } + Filestat filestat = Address.align(WasiBuffer.getBuffer(), 8).toStructure(); + int errno; + if (path != null) { + byte[] bytes = path.getBytes(StandardCharsets.UTF_8); + errno = Wasi.pathFilestatGet(baseFd, DIRFLAGS_FOLLOW_SYMLINKS, Address.ofData(bytes), bytes.length, + filestat); + } else { + errno = Wasi.fdFilestatGet(baseFd, filestat); + } + if (errno == ERRNO_SUCCESS) { + return new Stat((byte) filestat.fileType, filestat.lastModified, filestat.size); + } else { + return null; + } + } + + @Override + public boolean isDirectory() { + Stat stat = stat(); + return stat != null && stat.filetype == FILETYPE_DIRECTORY; + } + + @Override + public boolean isFile() { + Stat stat = stat(); + return stat != null && stat.filetype == FILETYPE_REGULAR_FILE; + } + + private String[] listFiles(int fd) { + SizeResult sizePtr = WasiBuffer.getBuffer().toStructure(); + ArrayList list = new ArrayList<>(); + final int direntSize = Structure.sizeOf(Dirent.class); + byte[] direntArray = fs.buffer; + Address direntBuffer = Address.align(Address.ofData(direntArray), 8); + int direntArrayOffset = (int) direntBuffer.diff(Address.ofData(direntArray).add(8)); + int bufferSize = direntArray.length - direntArrayOffset; + long cookie = 0; + + outer: + while (true) { + short errno = Wasi.fdReaddir(fd, direntBuffer, bufferSize, cookie, sizePtr); + if (errno != ERRNO_SUCCESS) { + return null; + } + int remainingSize = bufferSize; + while (true) { + if (remainingSize < direntSize) { + break; + } + Address direntPtr = direntBuffer.add(direntArrayOffset); + Dirent dirent = direntPtr.toStructure(); + int entryLength = direntSize + (int) dirent.nameLength; + if (entryLength > bufferSize) { + direntArray = new byte[entryLength * 3 / 2 + 8]; + direntBuffer = Address.align(Address.ofData(direntArray), 8); + direntArrayOffset = (int) direntBuffer.diff(Address.ofData(direntArray)); + bufferSize = direntArray.length - direntArrayOffset; + break; + } + cookie = dirent.next; + list.add(new String(direntArray, direntArrayOffset + direntSize, (int) dirent.nameLength, + StandardCharsets.UTF_8)); + remainingSize -= entryLength; + direntArrayOffset += entryLength; + if (direntArrayOffset == sizePtr.value) { + break outer; + } + } + } + + return list.toArray(new String[0]); + } + + @Override + public String[] listFiles() { + init(); + if (baseFd < 0) { + return null; + } + int fd = path != null ? open(OFLAGS_DIRECTORY, RIGHTS_READ, (short) 0) : baseFd; + if (fd < 0) { + return null; + } + try { + return listFiles(fd); + } finally { + if (path != null) { + close(fd); + } + } + } + private int open(int fd, String path, short oflags, long rights, short fdflags) { + byte[] bytes = path.getBytes(StandardCharsets.UTF_8); + FdResult fdPtr = WasiBuffer.getBuffer().toStructure(); + fs.errno = Wasi.pathOpen(fd, DIRFLAGS_FOLLOW_SYMLINKS, Address.ofData(bytes), bytes.length, oflags, + rights, rights, fdflags, fdPtr); + return fs.errno == ERRNO_SUCCESS ? fdPtr.value : -1; + } + + + private int open(short oflags, long rights, short fdflags) { + return open(baseFd, path, oflags, rights, fdflags); + } + + private static void close(int fd) { + // Ignore errno since this is only called in contexts where there's no need to handle the error. + Wasi.fdClose(fd); + } + + @Override + public VirtualFileAccessor createAccessor(boolean readable, boolean writable, boolean append) { + init(); + if (baseFd < 0) { + return null; + } + long rights = 0; + if (readable) { + rights |= RIGHTS_FD_FILESTAT_GET | RIGHTS_SEEK | RIGHTS_TELL | RIGHTS_READ; + } + if (writable) { + rights |= RIGHTS_FD_FILESTAT_SET_SIZE | RIGHTS_WRITE | RIGHTS_SYNC; + } + short fdflags = (short) 0; + if (append) { + fdflags |= FDFLAGS_APPEND; + } + int fd = open((short) 0, rights, fdflags); + return fd >= 0 ? new WasiVirtualFileAccessor(fs, fd) : null; + } + + @Override + public boolean createFile(String fileName) throws IOException { + init(); + if (baseFd < 0) { + return false; + } + int fd = open(baseFd, constructPath(path, fileName), (short) (OFLAGS_CREATE | OFLAGS_EXCLUSIVE), 0, (short) 0); + if (fs.errno == ERRNO_EXIST) { + return false; + } + if (fs.errno != ERRNO_SUCCESS) { + throw new IOException("fd_open: " + fs.errno); + } + close(fd); + return true; + } + + @Override + public boolean createDirectory(String fileName) { + init(); + if (baseFd < 0) { + return false; + } + String filePath = constructPath(path, fileName); + byte[] bytes = filePath.getBytes(StandardCharsets.UTF_8); + return Wasi.pathCreateDirectory(baseFd, Address.ofData(bytes), bytes.length) == ERRNO_SUCCESS; + } + + @Override + public boolean delete() { + init(); + if (fullPath == null || baseFd < 0) { + return false; + } + byte[] bytes = path.getBytes(StandardCharsets.UTF_8); + short errno = Wasi.pathUnlinkFile(baseFd, Address.ofData(bytes), bytes.length); + return errno == ERRNO_SUCCESS; + } + + private static boolean adopt(int fd, WasiVirtualFile file, String fileName) { + byte[] newBytes = fileName.getBytes(StandardCharsets.UTF_8); + byte[] oldBytes = file.path.getBytes(StandardCharsets.UTF_8); + short errno = Wasi.pathRename(file.baseFd, Address.ofData(oldBytes), oldBytes.length, fd, + Address.ofData(newBytes), newBytes.length); + return errno == ERRNO_SUCCESS; + } + + @Override + public boolean adopt(VirtualFile file, String fileName) { + if (!(file instanceof WasiVirtualFile)) { + return false; + } + WasiVirtualFile wasiFile = (WasiVirtualFile) file; + wasiFile.init(); + + if (wasiFile.path == null || fileName == null) { + return false; + } + + init(); + if (baseFd < 0) { + return false; + } + + if (path == null) { + return adopt(baseFd, wasiFile, fileName); + } else { + int fd = open(OFLAGS_DIRECTORY, RIGHTS_READ | RIGHTS_WRITE, (short) 0); + try { + return adopt(fd, wasiFile, fileName); + } finally { + close(fd); + } + } + } + + @Override + public boolean canRead() { + init(); + int fd = open((short) 0, RIGHTS_READ, (short) 0); + if (fd >= 0) { + close(fd); + return true; + } + return false; + } + + @Override + public boolean canWrite() { + init(); + int fd = open((short) 0, RIGHTS_WRITE, (short) 0); + if (fd >= 0) { + close(fd); + return true; + } + return false; + } + + @Override + public long lastModified() { + Stat stat = stat(); + return stat == null ? 0 : stat.mtime / 1000000L; + } + + @Override + public boolean setLastModified(long lastModified) { + init(); + if (baseFd < 0) { + return false; + } + + short errno; + if (path == null) { + errno = Wasi.fdFilestatSetTimes(baseFd, 0, lastModified * 1000000L, FSTFLAGS_MTIME); + } else { + byte[] bytes = path.getBytes(StandardCharsets.UTF_8); + errno = Wasi.pathFilestatSetTimes(baseFd, DIRFLAGS_FOLLOW_SYMLINKS, Address.ofData(bytes), bytes.length, + 0, lastModified * 1000000L, FSTFLAGS_MTIME); + } + + return errno == ERRNO_SUCCESS; + } + + @Override + public boolean setReadOnly(boolean readOnly) { + // TODO: is this possible on WASI? + return false; + } + + @Override + public int length() { + Stat stat = stat(); + return stat == null ? 0 : (int) stat.filesize; + } + + private String constructPath(String parent, String child) { + if (parent == null) { + return child; + } + return !parent.isEmpty() && parent.charAt(parent.length() - 1) == '/' + ? parent + child + : parent + '/' + child; + } + + static class Stat { + final byte filetype; + final long mtime; + final long filesize; + + Stat(byte filetype, long mtime, long filesize) { + this.filetype = filetype; + this.mtime = mtime; + this.filesize = filesize; + } + } +} \ No newline at end of file diff --git a/core/src/main/java/org/teavm/backend/wasm/runtime/fs/WasiVirtualFileAccessor.java b/core/src/main/java/org/teavm/backend/wasm/runtime/fs/WasiVirtualFileAccessor.java new file mode 100644 index 000000000..1df7433ef --- /dev/null +++ b/core/src/main/java/org/teavm/backend/wasm/runtime/fs/WasiVirtualFileAccessor.java @@ -0,0 +1,149 @@ +/* + * Copyright 2022 TeaVM Contributors. + * + * 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.runtime.fs; + +import java.io.IOException; +import org.teavm.backend.wasm.runtime.WasiBuffer; +import org.teavm.interop.Address; +import org.teavm.runtime.fs.VirtualFileAccessor; + +public class WasiVirtualFileAccessor implements VirtualFileAccessor { + // Enough room for an I32 plus padding for alignment: + 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; + + public WasiVirtualFileAccessor(WasiFileSystem fs, int fd) { + this.fs = fs; + this.fd = fd; + } + + @Override + public int read(byte[] buffer, int offset, int length) throws IOException { + byte[] vecBuffer = SIXTEEN_BYTE_BUFFER; + Address vec = WasiBuffer.getBuffer(); + vec.putInt(Address.ofData(buffer).add(offset).toInt()); + vec.add(4).putInt(length); + byte[] sizeBuffer = EIGHT_BYTE_BUFFER; + Address size = Address.align(Address.ofData(sizeBuffer), 4); + short errno = Wasi.fdRead(fd, vec, 1, size); + + if (errno == ERRNO_SUCCESS) { + return size.getInt(); + } else { + throw new IOException(errnoMessage("fd_read", errno)); + } + } + + @Override + public void write(byte[] buffer, int offset, int length) throws IOException { + byte[] vecBuffer = SIXTEEN_BYTE_BUFFER; + Address vec = Address.align(Address.ofData(vecBuffer), 4); + byte[] sizeBuffer = EIGHT_BYTE_BUFFER; + Address size = Address.align(Address.ofData(sizeBuffer), 4); + + int index = 0; + while (true) { + vec.putInt(Address.ofData(buffer).add(offset + index).toInt()); + vec.add(4).putInt(length - index); + short errno = Wasi.fdWrite(fd, vec, 1, size); + + if (errno == ERRNO_SUCCESS) { + index += size.getInt(); + if (index >= length) { + return; + } + } else { + throw new IOException(errnoMessage("fd_write", errno)); + } + } + } + + @Override + public int tell() throws IOException { + byte[] filesizeBuffer = SIXTEEN_BYTE_BUFFER; + Address filesize = Address.align(Address.ofData(filesizeBuffer), 8); + short errno = Wasi.fdTell(fd, filesize); + + if (errno == ERRNO_SUCCESS) { + return (int) filesize.getLong(); + } else { + throw new IOException(errnoMessage("fd_tell", errno)); + } + } + + private long seek(long offset, byte whence) throws IOException { + byte[] filesizeBuffer = SIXTEEN_BYTE_BUFFER; + Address filesize = Address.align(Address.ofData(filesizeBuffer), 8); + short errno = Wasi.fdSeek(fd, offset, whence, filesize); + + if (errno == ERRNO_SUCCESS) { + return filesize.getLong(); + } else { + throw new IOException(errnoMessage("fd_seek", errno)); + } + } + + @Override + public void skip(int amount) throws IOException { + seek(amount, WHENCE_CURRENT); + } + + @Override + public void seek(int target) throws IOException { + seek(target, WHENCE_START); + } + + @Override + public int size() throws IOException { + return (int) new WasiVirtualFile(fd, null).stat().filesize; + } + + @Override + public void resize(int size) throws IOException { + short errno = Wasi.fdFilestatSetSize(fd, size); + + if (errno != ERRNO_SUCCESS) { + throw new IOException(errnoMessage("fd_filestat_set_size", errno)); + } + } + + @Override + public void close() throws IOException { + if (this.fd >= 0) { + int fd = this.fd; + this.fd = -1; + + short errno = Wasi.fdClose(fd); + + if (errno != ERRNO_SUCCESS) { + throw new IOException(errnoMessage("fd_close", errno)); + } + } + } + + @Override + public void flush() throws IOException { + short errno = Wasi.fdSync(fd); + + if (errno != ERRNO_SUCCESS) { + throw new IOException(errnoMessage("fd_sync", errno)); + } + } +} \ No newline at end of file diff --git a/core/src/main/java/org/teavm/backend/wasm/wasi/Dirent.java b/core/src/main/java/org/teavm/backend/wasm/wasi/Dirent.java new file mode 100644 index 000000000..44776945b --- /dev/null +++ b/core/src/main/java/org/teavm/backend/wasm/wasi/Dirent.java @@ -0,0 +1,25 @@ +/* + * 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 Dirent extends Structure { + public long next; + public long inode; + public long nameLength; + public long fileType; +} diff --git a/core/src/main/java/org/teavm/backend/wasm/wasi/FdResult.java b/core/src/main/java/org/teavm/backend/wasm/wasi/FdResult.java new file mode 100644 index 000000000..600431102 --- /dev/null +++ b/core/src/main/java/org/teavm/backend/wasm/wasi/FdResult.java @@ -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 FdResult extends Structure { + public int value; +} diff --git a/core/src/main/java/org/teavm/backend/wasm/wasi/Filestat.java b/core/src/main/java/org/teavm/backend/wasm/wasi/Filestat.java new file mode 100644 index 000000000..494cb6b8f --- /dev/null +++ b/core/src/main/java/org/teavm/backend/wasm/wasi/Filestat.java @@ -0,0 +1,29 @@ +/* + * 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 Filestat extends Structure { + public long deviceId; + public long inode; + public long fileType; + public long linkCount; + public long size; + public long lastAccess; + public long lastModified; + public long lastStatusChange; +} diff --git a/core/src/main/java/org/teavm/backend/wasm/wasi/Prestat.java b/core/src/main/java/org/teavm/backend/wasm/wasi/Prestat.java new file mode 100644 index 000000000..620150554 --- /dev/null +++ b/core/src/main/java/org/teavm/backend/wasm/wasi/Prestat.java @@ -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 Prestat extends Structure { + public int kind; +} diff --git a/core/src/main/java/org/teavm/backend/wasm/wasi/PrestatDir.java b/core/src/main/java/org/teavm/backend/wasm/wasi/PrestatDir.java new file mode 100644 index 000000000..631ed929b --- /dev/null +++ b/core/src/main/java/org/teavm/backend/wasm/wasi/PrestatDir.java @@ -0,0 +1,20 @@ +/* + * 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; + +public class PrestatDir extends Prestat { + public int nameLength; +} diff --git a/core/src/main/java/org/teavm/backend/wasm/wasi/Wasi.java b/core/src/main/java/org/teavm/backend/wasm/wasi/Wasi.java index 1eb5c8ef1..b181eb6e7 100644 --- a/core/src/main/java/org/teavm/backend/wasm/wasi/Wasi.java +++ b/core/src/main/java/org/teavm/backend/wasm/wasi/Wasi.java @@ -20,14 +20,44 @@ import org.teavm.interop.Import; public final class Wasi { public static final int CLOCKID_REALTIME = 0; + + public static final byte PRESTAT_DIR = 0; + public static final short ERRNO_SUCCESS = 0; + public static final short ERRNO_BADF = 8; + public static final short ERRNO_EXIST = 20; + public static final short ERRNO_NOENT = 44; + + public static final byte FILETYPE_DIRECTORY = 3; + public static final byte FILETYPE_REGULAR_FILE = 4; + + public static final int DIRFLAGS_FOLLOW_SYMLINKS = 1; + + public static final short OFLAGS_CREATE = 1 << 0; + public static final short OFLAGS_DIRECTORY = 1 << 1; + public static final short OFLAGS_EXCLUSIVE = 1 << 2; + + public static final long RIGHTS_READ = 1L << 1; + public static final long RIGHTS_SEEK = 1L << 2; + public static final long RIGHTS_TELL = 1L << 5; + public static final long RIGHTS_WRITE = 1L << 6; + public static final long RIGHTS_SYNC = 1L << 4; + public static final long RIGHTS_CREATE_DIRECTORY = 1L << 9; + public static final long RIGHTS_CREATE_FILE = 1L << 10; + public static final long RIGHTS_FD_FILESTAT_GET = 1L << 21; + public static final long RIGHTS_FD_FILESTAT_SET_SIZE = 1L << 22; + + public static final short FDFLAGS_APPEND = 1 << 0; + + public static final short FSTFLAGS_MTIME = 1 << 2; + + public static final byte WHENCE_START = 0; + public static final byte WHENCE_CURRENT = 1; + public static final byte WHENCE_END = 2; private Wasi() { } - @Import(name = "fd_write", module = "wasi_snapshot_preview1") - public static native short fdWrite(int fd, IOVec vectors, int vectorsCont, SizeResult result); - @Import(name = "clock_time_get", module = "wasi_snapshot_preview1") public static native short clockTimeGet(int clockId, long precision, LongResult result); @@ -36,4 +66,47 @@ public final class Wasi { @Import(name = "args_get", module = "wasi_snapshot_preview1") public static native short argsGet(Address argv, Address argvBuf); + + @Import(name = "fd_write", module = "wasi_snapshot_preview1") + public static native short fdWrite(int fd, IOVec vectors, int vectorsCont, SizeResult result); + + @Import(name = "fd_prestat_get", module = "wasi_snapshot_preview1") + public static native short fdPrestatGet(int fd, Prestat prestat); + + @Import(name = "fd_prestat_dir_name", module = "wasi_snapshot_preview1") + public static native short fdPrestatDirName(int fd, Address buffer, int bufferLength); + + @Import(name = "fd_filestat_get", module = "wasi_snapshot_preview1") + public static native short fdFilestatGet(int fd, Filestat filestat); + + @Import(name = "path_filestat_get", module = "wasi_snapshot_preview1") + public static native short pathFilestatGet(int fd, int lookupFlags, Address path, int pathLength, + Filestat filestat); + + @Import(name = "fd_readdir", module = "wasi_snapshot_preview1") + public static native short fdReaddir(int fd, Address dirent, int direntSize, long cookie, SizeResult size); + + @Import(name = "path_open", module = "wasi_snapshot_preview1") + public static native short pathOpen(int dirFd, int lookupFlags, Address path, int pathLength, short oflags, + long baseRights, long inheritingRights, short fdflags, FdResult fd); + + @Import(name = "fd_close", module = "wasi_snapshot_preview1") + public static native short fdClose(int fd); + + @Import(name = "path_create_directory", module = "wasi_snapshot_preview1") + public static native short pathCreateDirectory(int fd, Address path, int pathLength); + + @Import(name = "path_unlink_file", module = "wasi_snapshot_preview1") + public static native short pathUnlinkFile(int fd, Address path, int pathLength); + + @Import(name = "path_rename", module = "wasi_snapshot_preview1") + public static native short pathRename(int oldFd, Address oldPath, int oldPathLength, int newFd, Address newPath, + int newPathLength); + + @Import(name = "fd_filestat_set_times", module = "wasi_snapshot_preview1") + public static native short fdFilestatSetTimes(int fd, long atime, long mtime, short fstflags); + + @Import(name = "path_filestat_set_times", module = "wasi_snapshot_preview1") + public static native short pathFilestatSetTimes(int fd, int lookupFlags, Address path, int pathLength, + long atime, long mtime, short fstflags); } diff --git a/interop/core/src/main/java/org/teavm/interop/Address.java b/interop/core/src/main/java/org/teavm/interop/Address.java index 75ff7a0bb..01faa8f50 100644 --- a/interop/core/src/main/java/org/teavm/interop/Address.java +++ b/interop/core/src/main/java/org/teavm/interop/Address.java @@ -93,4 +93,6 @@ public final class Address { public long diff(Address that) { return toLong() - that.toLong(); } + + public static native void pin(Object obj); }