From 7deeb464e495e991fd5c9551591e9108e78d2414 Mon Sep 17 00:00:00 2001 From: Wojciech Mazur Date: Thu, 5 Aug 2021 16:26:31 +0200 Subject: [PATCH] Allow to run unit-tests infrastructure on Windows (#2287) This PR continues topic of Scala Native support for Windows. It adds platform-specific compact layer for java.net Sockets allowing to execute unit tests. It also adds proper implementation for required java.io methods. To limit the number of changes currently only smaller subset of tests testsExt was added to CI * Included testsExt tests in the CI * Added minimal java.net implementation * Added binding for time.h not POSIX compliment *_s methods used in Windows * Added minimal java.io.File implementation required for tests * Added bindings for required WinSock2 and FileAPI functions * Fixed parsing symbols in Stacktrace on Windows * Added -g to compilation and linking step on Windows to allow gathering stack traces. * Fixed java.util.Date to compile on Windows --- .github/workflows/run-tests-windows.yml | 11 +- javalib/src/main/scala/java/io/File.scala | 165 +++++++++---- .../main/scala/java/io/FileOutputStream.scala | 3 +- .../scala/java/lang/StackTraceElement.scala | 6 +- ...pl.scala => AbstractPlainSocketImpl.scala} | 217 +++++------------- .../main/scala/java/net/ServerSocket.scala | 4 +- javalib/src/main/scala/java/net/Socket.scala | 22 +- .../src/main/scala/java/net/SocketImpl.scala | 2 +- .../scala/java/net/SocketInputStream.scala | 4 +- .../scala/java/net/SocketOutputStream.scala | 2 +- .../scala/java/net/UnixPlainSocketImpl.scala | 137 +++++++++++ .../java/net/WindowsPlainSocketImpl.scala | 118 ++++++++++ javalib/src/main/scala/java/util/Date.scala | 22 +- .../java/util/WindowsHelperMethods.scala | 36 ++- .../scalanative/nio/fs/FileHelpers.scala | 88 +++++-- .../scala/scala/scalanative/build/LLVM.scala | 10 +- .../resources/scala-native/windows/time.c | 129 +++++++++++ .../scala-native/windows/winSocket.c | 14 ++ .../scala/scalanative/windows/FileApi.scala | 24 +- .../scala/scalanative/windows/HandleApi.scala | 4 +- .../scalanative/windows/WinSocketApi.scala | 114 +++++++++ .../scala/scalanative/windows/crt/time.scala | 50 ++++ .../scalanative/windows/util/Conversion.scala | 16 ++ 23 files changed, 946 insertions(+), 252 deletions(-) rename javalib/src/main/scala/java/net/{PlainSocketImpl.scala => AbstractPlainSocketImpl.scala} (71%) create mode 100644 javalib/src/main/scala/java/net/UnixPlainSocketImpl.scala create mode 100644 javalib/src/main/scala/java/net/WindowsPlainSocketImpl.scala create mode 100644 windowslib/src/main/resources/scala-native/windows/time.c create mode 100644 windowslib/src/main/resources/scala-native/windows/winSocket.c create mode 100644 windowslib/src/main/scala/scala/scalanative/windows/WinSocketApi.scala create mode 100644 windowslib/src/main/scala/scala/scalanative/windows/crt/time.scala create mode 100644 windowslib/src/main/scala/scala/scalanative/windows/util/Conversion.scala diff --git a/.github/workflows/run-tests-windows.yml b/.github/workflows/run-tests-windows.yml index c19b766740..7a57a0c492 100644 --- a/.github/workflows/run-tests-windows.yml +++ b/.github/workflows/run-tests-windows.yml @@ -81,8 +81,8 @@ jobs: $cSources | ForEach-Object {clang -std=gnu11 -c "$_"} $cppSources | ForEach-Object {clang++ -std=c++14 -c "$_"} - run-sandbox: - name: Run sandbox project + run-tests: + name: Run tests runs-on: ${{matrix.os}} strategy: fail-fast: false @@ -121,8 +121,11 @@ jobs: run: clang --version - name: Run sandbox - # Set heap limits manually, there seams to be bug in detection of available memory - # which results in undefined behavior at runtime run: | sbt ++${{matrix.scala}};sandbox/run shell: cmd + + - name: Run testsExt tests + run: | + sbt ++${{matrix.scala}};testsExt/test + shell: cmd diff --git a/javalib/src/main/scala/java/io/File.scala b/javalib/src/main/scala/java/io/File.scala index 0fa7edca7a..d74a92feea 100644 --- a/javalib/src/main/scala/java/io/File.scala +++ b/javalib/src/main/scala/java/io/File.scala @@ -2,17 +2,27 @@ package java.io import java.nio.file.{FileSystems, Path} import java.net.URI - +import java.nio.charset.StandardCharsets import scala.annotation.tailrec -import scalanative.annotation.stub -import scalanative.posix.{fcntl, limits, unistd, utime} +import scalanative.annotation.{alwaysinline, stub} +import scalanative.posix.{limits, unistd, utime} import scalanative.posix.sys.stat import scalanative.unsigned._ import scalanative.unsafe._ -import scalanative.libc._, stdlib._, stdio._, string._ +import scalanative.libc._ +import stdlib._ +import stdio._ +import string._ import scalanative.nio.fs.FileHelpers import scalanative.runtime.{DeleteOnExit, Platform} import unistd._ +import scala.scalanative.meta.LinktimeInfo.isWindows +import scala.scalanative.windows +import windows.FileApi._ +import windows.FileApiExt._ +import scala.scalanative.windows.WinBaseApi._ +import scala.scalanative.windows.winnt.AccessRights._ +import java.util.WindowsHelperMethods._ class File(_path: String) extends Serializable with Comparable[File] { import File._ @@ -20,8 +30,6 @@ class File(_path: String) extends Serializable with Comparable[File] { if (_path == null) throw new NullPointerException() private val path: String = fixSlashes(_path) private[io] val properPath: String = File.properPath(path) - private[io] val properPathBytes: Array[Byte] = - File.properPath(path).getBytes("UTF-8") def this(parent: String, child: String) = this( @@ -31,10 +39,8 @@ class File(_path: String) extends Serializable with Comparable[File] { def this(parent: File, child: String) = this(Option(parent).map(_.path).orNull, child) - def this(uri: URI) = { - this(uri.getPath()) - checkURI(uri) - } + def this(uri: URI) = + this(File.checkURI(uri).getPath()) def compareTo(file: File): Int = { if (caseSensitive) getPath().compareTo(file.getPath()) @@ -86,8 +92,8 @@ class File(_path: String) extends Serializable with Comparable[File] { } } - def exists(): Boolean = - Zone { implicit z => access(toCString(path), unistd.F_OK) == 0 } + @inline + def exists(): Boolean = FileHelpers.exists(path) def toPath(): Path = FileSystems.getDefault().getPath(this.getPath(), Array.empty) @@ -104,8 +110,14 @@ class File(_path: String) extends Serializable with Comparable[File] { private def deleteDirImpl(): Boolean = Zone { implicit z => remove(toCString(path)) == 0 } - private def deleteFileImpl(): Boolean = - Zone { implicit z => unlink(toCString(path)) == 0 } + private def deleteFileImpl(): Boolean = Zone { implicit z => + if (isWindows) { + setReadOnlyWindows(enabled = false) + DeleteFileW(toCWideStringUTF16LE(path)) + } else { + unlink(toCString(path)) == 0 + } + } override def equals(that: Any): Boolean = that match { @@ -140,7 +152,8 @@ class File(_path: String) extends Serializable with Comparable[File] { resolvedName } - /** Finds the canonical path for `path`. */ + /** Finds the canonical path for `path`. + */ private def simplifyNonExistingPath(path: String): String = path .split(separatorChar) @@ -186,13 +199,26 @@ class File(_path: String) extends Serializable with Comparable[File] { File.isAbsolute(path) def isDirectory(): Boolean = - Zone { implicit z => stat.S_ISDIR(accessMode()) != 0 } + Zone { implicit z => + if (isWindows) + fileAttributeIsSet(FILE_ATTRIBUTE_DIRECTORY) + else + stat.S_ISDIR(accessMode()) != 0 + } def isFile(): Boolean = - Zone { implicit z => stat.S_ISREG(accessMode()) != 0 } + Zone { implicit z => + if (isWindows) + fileAttributeIsSet(FILE_ATTRIBUTE_NORMAL) || !isDirectory() + else + stat.S_ISREG(accessMode()) != 0 + } - def isHidden(): Boolean = - getName().startsWith(".") + def isHidden(): Boolean = { + if (isWindows) + fileAttributeIsSet(FILE_ATTRIBUTE_HIDDEN) + else getName().startsWith(".") + } def lastModified(): Long = Zone { implicit z => @@ -213,6 +239,12 @@ class File(_path: String) extends Serializable with Comparable[File] { } } + @alwaysinline + private def fileAttributeIsSet(attribute: windows.DWord): Boolean = Zone { + implicit z => + (GetFileAttributesW(toCWideStringUTF16LE(path)) & attribute) == attribute + } + def setLastModified(time: Long): Boolean = if (time < 0) { throw new IllegalArgumentException("Negative time") @@ -231,15 +263,37 @@ class File(_path: String) extends Serializable with Comparable[File] { def setReadOnly(): Boolean = Zone { implicit z => - import stat._ - val mask = - S_ISUID | S_ISGID | S_ISVTX | S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH - val newMode = accessMode() & mask - chmod(toCString(path), newMode) == 0 + if (isWindows) setReadOnlyWindows(enabled = true) + else { + import stat._ + val mask = + S_ISUID | S_ISGID | S_ISVTX | S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH + val newMode = accessMode() & mask + chmod(toCString(path), newMode) == 0 + } } - def length(): Long = - Zone { implicit z => + private def setReadOnlyWindows(enabled: Boolean)(implicit z: Zone) = { + val filename = toCWideStringUTF16LE(path) + val currentAttributes = GetFileAttributesW(filename) + + def newAttributes = + if (enabled) currentAttributes | FILE_ATTRIBUTE_READONLY + else currentAttributes & ~FILE_ATTRIBUTE_READONLY + + def setNewAttributes() = SetFileAttributesW(filename, newAttributes) + + currentAttributes != INVALID_FILE_ATTRIBUTES && setNewAttributes() + } + + def length(): Long = Zone { implicit z => + if (isWindows) { + withFileOpen(path, access = FILE_READ_ATTRIBUTES) { handle => + val size = stackalloc[windows.LargeInteger] + if (GetFileSizeEx(handle, size)) (!size).toLong + else 0L + } + } else { val buf = alloc[stat.stat] if (stat.stat(toCString(path), buf) == 0) { buf._6 @@ -247,6 +301,7 @@ class File(_path: String) extends Serializable with Comparable[File] { 0L } } + } def list(): Array[String] = list(FilenameFilter.allPassFilter) @@ -316,7 +371,7 @@ class File(_path: String) extends Serializable with Comparable[File] { // Ported from Apache Harmony def toURI(): URI = { - val path = getAbsolutePath() + val path = getAbsolutePath().replace('\\', '/') if (!path.startsWith("/")) { // start with sep. new URI( @@ -344,9 +399,16 @@ object File { private def getUserDir(): String = Zone { implicit z => - var buff: CString = alloc[CChar](4096.toUInt) - var res: CString = getcwd(buff, 4095.toUInt) - fromCString(res) + if (isWindows) { + val buffSize = GetCurrentDirectoryW(0.toUInt, null) + val buff = alloc[windows.WChar](buffSize + 1.toUInt) + GetCurrentDirectoryW(buffSize, buff) + fromCWideString(buff, StandardCharsets.UTF_16LE) + } else { + val buff: CString = alloc[CChar](4096.toUInt) + getcwd(buff, 4095.toUInt) + fromCString(buff) + } } /** The purpose of this method is to take a path and fix the slashes up. This @@ -379,10 +441,12 @@ object File { } } else { // check for leading slashes before a drive - if (currentChar == ':' && uncIndex > 0 && - (newLength == 2 || - (newLength == 3 && newPath(1) == separatorChar)) && - newPath(0) == separatorChar) { + if (currentChar == ':' + && uncIndex > 0 + && (newLength == 2 || (newLength == 3 && newPath( + 1 + ) == separatorChar)) + && newPath(0) == separatorChar) { newPath(0) = newPath(newLength - 1) newLength = 1 // allow trailing slash after drive letter @@ -396,9 +460,9 @@ object File { i += 1 } - if (foundSlash && - (newLength > (uncIndex + 1) || - (newLength == 2 && newPath(0) != separatorChar))) { + if (foundSlash && (newLength > (uncIndex + 1) || (newLength == 2 && newPath( + 0 + ) != separatorChar))) { newLength -= 1 } @@ -411,6 +475,20 @@ object File { // Ported from Apache Harmony private def properPath(path: String): String = { if (isAbsolute(path)) path + else if (isWindows) Zone { implicit z => + val pathCString = toCWideStringUTF16LE(path) + val bufSize = GetFullPathNameW(pathCString, 0.toUInt, null, null) + val buf = stackalloc[windows.WChar](bufSize) + if (GetFullPathNameW( + pathCString, + bufferLength = bufSize, + buffer = buf, + filePart = null + ) == 0.toUInt) { + throw new IOException("Failed to resolve correct path") + } + fromCWideString(buf, StandardCharsets.UTF_16LE) + } else { val userdir = Option(getUserDir()) @@ -429,8 +507,9 @@ object File { def isAbsolute(path: String): Boolean = if (separatorChar == '\\') { // Windows. Must start with `\\` or `X:(\|/)` (path.length > 1 && path.startsWith(separator + separator)) || - (path.length > 2 && path(0).isLetter && path(1) == ':' && - (path(2) == '/' || path(2) == '\\')) + (path.length > 2 && path(0).isLetter && path(1) == ':' && (path( + 2 + ) == '/' || path(2) == '\\')) } else { path.length > 0 && path.startsWith(separator) } @@ -480,7 +559,10 @@ object File { @tailrec private def resolve(path: CString, start: UInt = 0.toUInt)(implicit z: Zone ): CString = { - val part: CString = alloc[Byte](limits.PATH_MAX.toUInt) + val partSize = + if (isWindows) windows.FileApiExt.MAX_PATH + else limits.PATH_MAX.toUInt + val part: CString = alloc[Byte](partSize) val `1U` = 1.toUInt // Find the next separator var i = start @@ -554,7 +636,7 @@ object File { ) // Ported from Apache Harmony - private def checkURI(uri: URI): Unit = { + private def checkURI(uri: URI): URI = { def throwExc(msg: String): Unit = throw new IllegalArgumentException(s"$msg: $uri") def compMsg(comp: String): String = @@ -575,6 +657,7 @@ object File { } else if (uri.getRawFragment() != null) { throwExc(compMsg("fragment")) } + uri // else URI is ok } } diff --git a/javalib/src/main/scala/java/io/FileOutputStream.scala b/javalib/src/main/scala/java/io/FileOutputStream.scala index edbd30021f..2c5fc0fbc4 100644 --- a/javalib/src/main/scala/java/io/FileOutputStream.scala +++ b/javalib/src/main/scala/java/io/FileOutputStream.scala @@ -14,8 +14,9 @@ import scala.scalanative.windows.FileApiExt._ import scala.scalanative.windows.HandleApiExt._ import scala.scalanative.windows.winnt.AccessRights._ -class FileOutputStream(fd: FileDescriptor, file: Option[File] = None) +class FileOutputStream(fd: FileDescriptor, file: Option[File]) extends OutputStream { + def this(fd: FileDescriptor) = this(fd, None) def this(file: File, append: Boolean) = this(FileOutputStream.fileDescriptor(file, append), Some(file)) def this(file: File) = this(file, false) diff --git a/javalib/src/main/scala/java/lang/StackTraceElement.scala b/javalib/src/main/scala/java/lang/StackTraceElement.scala index ff49511e86..000d1b2c04 100644 --- a/javalib/src/main/scala/java/lang/StackTraceElement.scala +++ b/javalib/src/main/scala/java/lang/StackTraceElement.scala @@ -2,6 +2,7 @@ package java.lang import scalanative.unsafe.{CString, fromCString} import scalanative.libc.string.strlen +import scalanative.runtime.Platform.isWindows final class StackTraceElement( val getClassName: String, @@ -52,7 +53,10 @@ private[lang] object StackTraceElement { var methodName = "" def readSymbol(): Boolean = { - if (read() != '_') { + // On Windows symbol names are different then on Unix platforms. + // Due to differences in implementation between WinDbg and libUnwind used + // on each platform, symbols on Windows do not contain '_' prefix. + if (!isWindows() && read() != '_') { false } else if (read() != 'S') { false diff --git a/javalib/src/main/scala/java/net/PlainSocketImpl.scala b/javalib/src/main/scala/java/net/AbstractPlainSocketImpl.scala similarity index 71% rename from javalib/src/main/scala/java/net/PlainSocketImpl.scala rename to javalib/src/main/scala/java/net/AbstractPlainSocketImpl.scala index 948e6f4972..ff8577897b 100644 --- a/javalib/src/main/scala/java/net/PlainSocketImpl.scala +++ b/javalib/src/main/scala/java/net/AbstractPlainSocketImpl.scala @@ -7,45 +7,49 @@ import scala.scalanative.runtime.ByteArray import scala.scalanative.posix.errno._ import scala.scalanative.posix.sys.socket import scala.scalanative.posix.sys.socketOps._ +import scala.scalanative.posix.sys.ioctl._ import scala.scalanative.posix.netinet.in import scala.scalanative.posix.netinet.inOps._ import scala.scalanative.posix.netinet.tcp import scala.scalanative.posix.arpa.inet import scala.scalanative.posix.netdb._ import scala.scalanative.posix.netdbOps._ -import scala.scalanative.posix.poll._ -import scala.scalanative.posix.pollEvents._ -import scala.scalanative.posix.pollOps._ -import scala.scalanative.posix.sys.ioctl._ -import scala.scalanative.posix.fcntl._ -import scala.scalanative.posix.sys.select._ import scala.scalanative.posix.sys.time._ import scala.scalanative.posix.sys.timeOps._ -import scala.scalanative.posix.unistd.{close => cClose} +import scala.scalanative.meta.LinktimeInfo.isWindows import java.io.{FileDescriptor, IOException, OutputStream, InputStream} -private[net] class PlainSocketImpl extends SocketImpl { +private[net] abstract class AbstractPlainSocketImpl extends SocketImpl { + import AbstractPlainSocketImpl._ + + protected def setSocketFdBlocking(fd: FileDescriptor, blocking: Boolean): Unit + protected def tryPollOnConnect(timeout: Int): Unit + protected def tryPollOnAccept(): Unit protected[net] var fd = new FileDescriptor protected[net] var localport = 0 protected[net] var address: InetAddress = null protected[net] var port = 0 - private var timeout = 0 + protected var timeout = 0 private var listening = false override def getInetAddress: InetAddress = address override def getFileDescriptor: FileDescriptor = fd + final protected var isClosed: Boolean = + fd == InvalidSocketDescriptor - private def throwIfClosed(osFd: Int, methodName: String): Unit = { - if (osFd == -1) { - throw new SocketException(s"${methodName}: Socket is closed") + private def throwIfClosed(methodName: String): Unit = { + if (isClosed) { + throw new SocketException(s"$methodName: Socket is closed") } } - override def create(streaming: Boolean): Unit = { - val sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) - if (sock < 0) throw new IOException("Couldn't create a socket") - fd = new FileDescriptor(sock) + + private def throwCannotBind(addr: InetAddress): Nothing = { + throw new BindException( + "Couldn't bind to an address: " + addr.getHostAddress() + + " on port: " + port.toString + ) } private def fetchLocalPort(family: Int): Option[Int] = { @@ -103,17 +107,11 @@ private[net] class PlainSocketImpl extends SocketImpl { freeaddrinfo(!ret) if (bindRes < 0) { - throw new BindException( - "Couldn't bind to an address: " + addr.getHostAddress() + - " on port: " + port.toString - ) + throwCannotBind(addr) } this.localport = fetchLocalPort(family).getOrElse { - throw new BindException( - "Couldn't bind to address: " + addr.getHostAddress() + - " on port: " + port - ) + throwCannotBind(addr) } } @@ -125,48 +123,10 @@ private[net] class PlainSocketImpl extends SocketImpl { } override def accept(s: SocketImpl): Unit = { - - throwIfClosed(fd.fd, "accept") // Do not send negative fd.fd to poll() + throwIfClosed("accept") // Do not send negative fd.fd to poll() if (timeout > 0) { - val nAlloc = 1.toUInt - - val pollFdPtr = stackalloc[struct_pollfd](nAlloc) - - pollFdPtr.fd = fd.fd - pollFdPtr.events = POLLIN - pollFdPtr.revents = 0 - - val pollRes = poll(pollFdPtr, nAlloc, timeout) - - pollRes match { - case err if err < 0 => - throw new SocketException( - s"accept failed, poll errno: ${errno.errno}" - ) - - case 0 => - throw new SocketTimeoutException( - s"accept timed out, SO_TIMEOUT: ${timeout}" - ) - - case _ => // success, carry on - } - - val revents = pollFdPtr.revents - - if (((revents & POLLERR) | (revents & POLLHUP)) != 0) { - throw new SocketException("Accept poll failed, POLLERR or POLLHUP") - } else if ((revents & POLLNVAL) != 0) { - throw new SocketException( - s"accept failed, invalid poll request: ${revents}" - ) - } else if (((revents & POLLIN) | (revents & POLLOUT)) == 0) { - throw new SocketException( - "accept failed, neither POLLIN nor POLLOUT set, " + - s"revents, ${revents}" - ) - } + tryPollOnAccept() } val storage = stackalloc[Byte](sizeof[in.sockaddr_in6]) @@ -216,81 +176,10 @@ private[net] class PlainSocketImpl extends SocketImpl { override def connect(address: InetAddress, port: Int): Unit = { connect(new InetSocketAddress(address, port), 0) } - @inline - private def connectGetFdOpts(fdFd: Int): Int = { - val opts = fcntl(fdFd, F_GETFL, 0) - - if (opts == -1) { - throw new ConnectException( - "connect failed, fcntl F_GETFL" + - s", errno: ${errno.errno}" - ) - } - - opts - } - - @inline - private def connectSetFdOpts(fdFd: Int, opts: Int): Unit = { - val ret = fcntl(fdFd, F_SETFL, opts) - - if (ret == -1) { - throw new ConnectException( - "connect failed, " + - s"fcntl F_SETFL for opts: ${opts}" + - s", errno: ${errno.errno}" - ) - } - } - - @inline - private def connectSetFdNoBlock(fdFd: Int): Int = { - val oldOpts = connectGetFdOpts(fdFd) - val newOpts = oldOpts | O_NONBLOCK - connectSetFdOpts(fdFd, newOpts) - oldOpts - } - - private def connectPollTimeout(timeout: Int, fdFd: Int, opts: Int): Unit = { - val nAlloc = 1.toUInt - - val pollFdPtr = stackalloc[struct_pollfd](nAlloc) - - pollFdPtr.fd = fd.fd - pollFdPtr.events = POLLIN | POLLOUT - pollFdPtr.revents = 0 - - val pollRes = poll(pollFdPtr, nAlloc.toUInt, timeout) - - connectSetFdOpts(fdFd, opts & ~O_NONBLOCK) - - pollRes match { - case err if err < 0 => - throw new SocketException(s"connect failed, poll errno: ${errno.errno}") - - case 0 => - throw new SocketTimeoutException( - s"connect timed out, SO_TIMEOUT: ${timeout}" - ) - - case _ => - val revents = pollFdPtr.revents - - if ((revents & POLLNVAL) != 0) { - throw new ConnectException( - s"connect failed, invalid poll request: ${revents}" - ) - } else if ((revents & (POLLERR | POLLHUP)) != 0) { - throw new ConnectException( - s"connect failed, POLLERR or POLLHUP set: ${revents}" - ) - } - } - } override def connect(address: SocketAddress, timeout: Int): Unit = { - throwIfClosed(fd.fd, "connect") // Do not send negative fd.fd to poll() + throwIfClosed("connect") // Do not send negative fd.fd to poll() val inetAddr = address.asInstanceOf[InetSocketAddress] val hints = stackalloc[addrinfo] @@ -317,7 +206,8 @@ private[net] class PlainSocketImpl extends SocketImpl { val family = (!ret).ai_family - val oldOpts = if (timeout == 0) 0 else connectSetFdNoBlock(fd.fd) + if (timeout != 0) + setSocketFdBlocking(fd, blocking = false) val connectRet = socket.connect(fd.fd, (!ret).ai_addr, (!ret).ai_addrlen) @@ -325,7 +215,7 @@ private[net] class PlainSocketImpl extends SocketImpl { if (connectRet < 0) { if ((timeout > 0) && (errno.errno == EINPROGRESS)) { - connectPollTimeout(timeout, fd.fd, oldOpts) + tryPollOnConnect(timeout) } else { throw new ConnectException( s"Could not connect to address: ${remoteAddress}" @@ -345,14 +235,15 @@ private[net] class PlainSocketImpl extends SocketImpl { } override def close(): Unit = { - if (fd.fd != -1) { - cClose(fd.fd) - fd = new FileDescriptor + if (!isClosed) { + fd.close() + fd = InvalidSocketDescriptor + isClosed = true } } override def getOutputStream: OutputStream = { - if (fd.fd == -1) { + if (isClosed) { throw new SocketException("Socket is closed") } if (shutOutput) { @@ -362,7 +253,7 @@ private[net] class PlainSocketImpl extends SocketImpl { } override def getInputStream: InputStream = { - if (fd.fd == -1) { + if (isClosed) { throw new SocketException("Socket is closed") } if (shutInput) { @@ -390,7 +281,7 @@ private[net] class PlainSocketImpl extends SocketImpl { def write(buffer: Array[Byte], offset: Int, count: Int): Int = { if (shutOutput) { throw new IOException("Trying to write to a shut down socket") - } else if (fd.fd == -1) { + } else if (isClosed) { 0 } else { val cArr = buffer.asInstanceOf[ByteArray].at(offset) @@ -410,25 +301,26 @@ private[net] class PlainSocketImpl extends SocketImpl { def read(buffer: Array[Byte], offset: Int, count: Int): Int = { if (shutInput) -1 + else { + val bytesNum = socket + .recv(fd.fd, buffer.asInstanceOf[ByteArray].at(offset), count.toUInt, 0) + .toInt - val bytesNum = socket - .recv(fd.fd, buffer.asInstanceOf[ByteArray].at(offset), count.toUInt, 0) - .toInt - - bytesNum match { - case _ if (bytesNum > 0) => bytesNum + bytesNum match { + case _ if (bytesNum > 0) => bytesNum - case 0 => if (count == 0) 0 else -1 + case 0 => if (count == 0) 0 else -1 - case _ if ((errno.errno == EAGAIN) || (errno.errno == EWOULDBLOCK)) => - throw new SocketTimeoutException("Socket timeout while reading data") + case _ if ((errno.errno == EAGAIN) || (errno.errno == EWOULDBLOCK)) => + throw new SocketTimeoutException("Socket timeout while reading data") - case _ => - throw new SocketException(s"read failed, errno: ${errno.errno}") + case _ => + throw new SocketException(s"read failed, errno: ${errno.errno}") + } } } - override def available: Int = { + override def available(): Int = { if (shutInput) { 0 } else { @@ -461,7 +353,7 @@ private[net] class PlainSocketImpl extends SocketImpl { } override def getOption(optID: Int): Object = { - if (fd.fd == -1) { + if (isClosed) { throw new SocketException("Socket is closed") } @@ -516,7 +408,7 @@ private[net] class PlainSocketImpl extends SocketImpl { } override def setOption(optID: Int, value: Object): Unit = { - if (fd.fd == -1) { + if (isClosed) { throw new SocketException("Socket is closed") } @@ -583,5 +475,14 @@ private[net] class PlainSocketImpl extends SocketImpl { ) } } +} + +private[net] object AbstractPlainSocketImpl { + final val InvalidSocketDescriptor = new FileDescriptor + def apply(): AbstractPlainSocketImpl = { + if (isWindows) new WindowsPlainSocketImpl() + else + new UnixPlainSocketImpl() + } } diff --git a/javalib/src/main/scala/java/net/ServerSocket.scala b/javalib/src/main/scala/java/net/ServerSocket.scala index 9096f8c088..c10f608411 100644 --- a/javalib/src/main/scala/java/net/ServerSocket.scala +++ b/javalib/src/main/scala/java/net/ServerSocket.scala @@ -1,6 +1,6 @@ package java.net -import java.io.{InputStream, OutputStream, IOException, Closeable} +import java.io.Closeable class ServerSocket( private var port: Int, @@ -8,7 +8,7 @@ class ServerSocket( private var bindAddr: InetAddress ) extends Closeable { - private val impl = new PlainSocketImpl() + private val impl = AbstractPlainSocketImpl() private var created = false private var bound = false diff --git a/javalib/src/main/scala/java/net/Socket.scala b/javalib/src/main/scala/java/net/Socket.scala index 2bcbdf7abc..150ea46f70 100644 --- a/javalib/src/main/scala/java/net/Socket.scala +++ b/javalib/src/main/scala/java/net/Socket.scala @@ -52,13 +52,13 @@ class Socket protected ( } def this() = - this(new PlainSocketImpl(), null, -1, null, 0, true, false) + this(AbstractPlainSocketImpl(), null, -1, null, 0, true, false) protected[net] def this(impl: SocketImpl) = this(impl, null, -1, null, 0, true, false) def this(address: InetAddress, port: Int) = - this(new PlainSocketImpl(), address, port, null, 0, true, true) + this(AbstractPlainSocketImpl(), address, port, null, 0, true, true) def this( address: InetAddress, @@ -66,11 +66,19 @@ class Socket protected ( localAddr: InetAddress, localPort: Int ) = - this(new PlainSocketImpl(), address, port, localAddr, localPort, true, true) + this( + AbstractPlainSocketImpl(), + address, + port, + localAddr, + localPort, + true, + true + ) def this(host: String, port: Int) = this( - new PlainSocketImpl(), + AbstractPlainSocketImpl(), InetAddress.getByName(host), port, null, @@ -81,7 +89,7 @@ class Socket protected ( def this(host: String, port: Int, localAddr: InetAddress, localPort: Int) = this( - new PlainSocketImpl(), + AbstractPlainSocketImpl(), InetAddress.getByName(host), port, localAddr, @@ -91,11 +99,11 @@ class Socket protected ( ) def this(host: InetAddress, port: Int, stream: Boolean) = - this(new PlainSocketImpl(), host, port, null, 0, stream, true) + this(AbstractPlainSocketImpl(), host, port, null, 0, stream, true) def this(host: String, port: Int, stream: Boolean) = this( - new PlainSocketImpl(), + AbstractPlainSocketImpl(), InetAddress.getByName(host), port, null, diff --git a/javalib/src/main/scala/java/net/SocketImpl.scala b/javalib/src/main/scala/java/net/SocketImpl.scala index fc8c4e0771..ee80017da1 100644 --- a/javalib/src/main/scala/java/net/SocketImpl.scala +++ b/javalib/src/main/scala/java/net/SocketImpl.scala @@ -11,7 +11,7 @@ abstract class SocketImpl extends SocketOptions { protected[net] var shutOutput = false protected[net] def accept(s: SocketImpl): Unit - protected[net] def available: Int + protected[net] def available(): Int protected[net] def bind(host: InetAddress, port: Int): Unit protected[net] def close(): Unit protected[net] def connect(address: InetAddress, port: Int): Unit diff --git a/javalib/src/main/scala/java/net/SocketInputStream.scala b/javalib/src/main/scala/java/net/SocketInputStream.scala index 26344b0ad8..6bb6b84446 100644 --- a/javalib/src/main/scala/java/net/SocketInputStream.scala +++ b/javalib/src/main/scala/java/net/SocketInputStream.scala @@ -3,12 +3,12 @@ package java.net import java.io.InputStream // Ported from Apache Harmony -private[net] class SocketInputStream(socket: PlainSocketImpl) +private[net] class SocketInputStream(socket: AbstractPlainSocketImpl) extends InputStream { override def close(): Unit = socket.close() - override def available(): Int = socket.available + override def available(): Int = socket.available() override def read(): Int = { val buffer = new Array[Byte](1) diff --git a/javalib/src/main/scala/java/net/SocketOutputStream.scala b/javalib/src/main/scala/java/net/SocketOutputStream.scala index 91f08e72e8..c46e94174b 100644 --- a/javalib/src/main/scala/java/net/SocketOutputStream.scala +++ b/javalib/src/main/scala/java/net/SocketOutputStream.scala @@ -3,7 +3,7 @@ package java.net import java.io.OutputStream // Ported from Apache Harmony -private[net] class SocketOutputStream(socket: PlainSocketImpl) +private[net] class SocketOutputStream(socket: AbstractPlainSocketImpl) extends OutputStream { override def close(): Unit = { diff --git a/javalib/src/main/scala/java/net/UnixPlainSocketImpl.scala b/javalib/src/main/scala/java/net/UnixPlainSocketImpl.scala new file mode 100644 index 0000000000..c07b0da41e --- /dev/null +++ b/javalib/src/main/scala/java/net/UnixPlainSocketImpl.scala @@ -0,0 +1,137 @@ +package java.net + +import scala.scalanative.unsigned._ +import scala.scalanative.unsafe._ +import scala.scalanative.libc._ +import scala.scalanative.posix.fcntl._ +import scala.scalanative.posix.poll._ +import scala.scalanative.posix.pollEvents._ +import scala.scalanative.posix.pollOps._ +import scala.scalanative.posix.sys.socket + +import java.io.{FileDescriptor, IOException} + +private[net] class UnixPlainSocketImpl extends AbstractPlainSocketImpl { + + override def create(streaming: Boolean): Unit = { + val sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) + if (sock < 0) throw new IOException("Couldn't create a socket") + fd = new FileDescriptor(sock) + } + + protected def tryPollOnConnect(timeout: Int): Unit = { + val nAlloc = 1.toUInt + val pollFd = stackalloc[struct_pollfd](nAlloc) + + pollFd.fd = fd.fd + pollFd.revents = 0 + pollFd.events = (POLLIN | POLLOUT).toShort + + val pollRes = poll(pollFd, nAlloc, timeout) + val revents = pollFd.revents + + setSocketFdBlocking(fd, blocking = true) + + pollRes match { + case err if err < 0 => + throw new SocketException(s"connect failed, poll errno: ${errno.errno}") + + case 0 => + throw new SocketTimeoutException( + s"connect timed out, SO_TIMEOUT: ${timeout}" + ) + + case _ => + if ((revents & POLLNVAL) != 0) { + throw new ConnectException( + s"connect failed, invalid poll request: ${revents}" + ) + } else if ((revents & (POLLERR | POLLHUP)) != 0) { + throw new ConnectException( + s"connect failed, POLLERR or POLLHUP set: ${revents}" + ) + } + } + } + + protected def tryPollOnAccept(): Unit = { + val nAlloc = 1.toUInt + val pollFd = stackalloc[struct_pollfd](nAlloc) + + pollFd.fd = fd.fd + pollFd.revents = 0 + pollFd.events = POLLIN + + val pollRes = poll(pollFd, nAlloc, timeout) + val revents = pollFd.revents + + pollRes match { + case err if err < 0 => + throw new SocketException(s"accept failed, poll errno: ${errno.errno}") + + case 0 => + throw new SocketTimeoutException( + s"accept timed out, SO_TIMEOUT: ${timeout}" + ) + + case _ => // success, carry on + } + + if (((revents & POLLERR) | (revents & POLLHUP)) != 0) { + throw new SocketException("Accept poll failed, POLLERR or POLLHUP") + } else if ((revents & POLLNVAL) != 0) { + throw new SocketException( + s"accept failed, invalid poll request: ${revents}" + ) + } else if (((revents & POLLIN) | (revents & POLLOUT)) == 0) { + throw new SocketException( + "accept failed, neither POLLIN nor POLLOUT set, " + + s"revents, ${revents}" + ) + } + } + + protected def setSocketFdBlocking( + fd: FileDescriptor, + blocking: Boolean + ): Unit = { + updateSocketFdOpts(fd.fd) { oldOpts => + if (blocking) oldOpts & ~O_NONBLOCK + else oldOpts | O_NONBLOCK + } + } + + @inline + private def getSocketFdOpts(fdFd: Int): CInt = { + val opts = fcntl(fdFd, F_GETFL, 0) + + if (opts == -1) { + throw new ConnectException( + "connect failed, fcntl F_GETFL" + + s", errno: ${errno.errno}" + ) + } + + opts + } + + @inline + private def setSocketFdOpts(fdFd: Int, opts: Int): Unit = { + val ret = fcntl(fdFd, F_SETFL, opts) + + if (ret == -1) { + throw new ConnectException( + "connect failed, " + + s"fcntl F_SETFL for opts: ${opts}" + + s", errno: ${errno.errno}" + ) + } + } + + @inline + private def updateSocketFdOpts(fdFd: Int)(mapping: CInt => CInt): Int = { + val oldOpts = getSocketFdOpts(fdFd) + setSocketFdOpts(fdFd, mapping(oldOpts)) + oldOpts + } +} diff --git a/javalib/src/main/scala/java/net/WindowsPlainSocketImpl.scala b/javalib/src/main/scala/java/net/WindowsPlainSocketImpl.scala new file mode 100644 index 0000000000..f237cbe7f7 --- /dev/null +++ b/javalib/src/main/scala/java/net/WindowsPlainSocketImpl.scala @@ -0,0 +1,118 @@ +package java.net + +import java.io.{FileDescriptor, IOException} +import scala.scalanative.libc._ +import scala.scalanative.posix.sys.{socket => unixSocket} +import scala.scalanative.unsafe._ +import scala.scalanative.unsigned._ +import scala.scalanative.windows._ + +private[net] class WindowsPlainSocketImpl extends AbstractPlainSocketImpl { + import WinSocketApi._ + import WinSocketApiExt._ + import WinSocketApiOps._ + + override def create(streaming: Boolean): Unit = { + WinSocketApiOps.init() + val socket = WSASocketW( + addressFamily = unixSocket.AF_INET, + socketType = unixSocket.SOCK_STREAM, + protocol = 0, // choosed by provider + protocolInfo = null, + group = 0.toUInt, + flags = 0.toUInt + ) + if (socket == InvalidSocket) { + throw new IOException(s"Couldn't create a socket: ${WSAGetLastError()}") + } + fd = new FileDescriptor( + FileDescriptor.FileHandle(socket), + readOnly = false + ) + } + + protected def tryPollOnConnect(timeout: Int): Unit = { + val nAlloc = 1.toUInt + val pollFd = stackalloc[WSAPollFd](nAlloc) + + pollFd.socket = fd.handle + pollFd.revents = 0.toShort + pollFd.events = (POLLIN | POLLOUT).toShort + + val pollRes = WSAPoll(pollFd, nAlloc, timeout) + val revents = pollFd.revents + + setSocketFdBlocking(fd, blocking = true) + + pollRes match { + case err if err < 0 => + throw new SocketException(s"connect failed, poll errno: ${errno.errno}") + + case 0 => + throw new SocketTimeoutException( + s"connect timed out, SO_TIMEOUT: ${timeout}" + ) + + case _ => + if ((revents & POLLNVAL) != 0) { + throw new ConnectException( + s"connect failed, invalid poll request: ${revents}" + ) + } else if ((revents & (POLLERR | POLLHUP)) != 0) { + throw new ConnectException( + s"connect failed, POLLERR or POLLHUP set: ${revents}" + ) + } + } + } + + protected def tryPollOnAccept(): Unit = { + val nAlloc = 1.toUInt + val pollFd = stackalloc[WSAPollFd](nAlloc) + + pollFd.socket = fd.handle + pollFd.revents = 0.toShort + pollFd.events = POLLIN.toShort + + val pollRes = WSAPoll(pollFd, nAlloc, timeout) + val revents = pollFd.revents + + pollRes match { + case err if err < 0 => + throw new SocketException(s"accept failed, poll errno: ${errno.errno}") + + case 0 => + throw new SocketTimeoutException( + s"accept timed out, SO_TIMEOUT: ${timeout}" + ) + + case _ => // success, carry on + } + + if (((revents & POLLERR) | (revents & POLLHUP)) != 0) { + throw new SocketException("Accept poll failed, POLLERR or POLLHUP") + } else if ((revents & POLLNVAL) != 0) { + throw new SocketException( + s"accept failed, invalid poll request: ${revents}" + ) + } else if (((revents & POLLIN) | (revents & POLLOUT)) == 0) { + throw new SocketException( + "accept failed, neither POLLIN nor POLLOUT set, " + + s"revents, ${revents}" + ) + } + } + + protected def setSocketFdBlocking( + fd: FileDescriptor, + blocking: Boolean + ): Unit = { + val mode = stackalloc[Int] + if (blocking) + !mode = 1 + else + !mode = 0 + ioctlSocket(fd.handle, FIONBIO, mode) + } + +} diff --git a/javalib/src/main/scala/java/util/Date.scala b/javalib/src/main/scala/java/util/Date.scala index 0f6a3939c0..6c696e973d 100644 --- a/javalib/src/main/scala/java/util/Date.scala +++ b/javalib/src/main/scala/java/util/Date.scala @@ -3,9 +3,10 @@ package java.util import java.time.Instant import scalanative.posix.time._ -import scalanative.posix.sys.types.size_t +import scalanative.windows.crt.{time => winTime} import scalanative.unsafe._ import scalanative.unsigned._ +import scala.scalanative.meta.LinktimeInfo.isWindows /** Ported from Scala JS and Apache Harmony * - omits deprecated methods @@ -54,8 +55,8 @@ object Date { // Applications which must track timezone changes over their lifetime // must do timely subsequent tzset() calls, either directly or through // an occasional localtime(). - - tzset() + if (isWindows) winTime.tzset() + else tzset() private def secondsToString(seconds: Long, default: => String): String = Zone { implicit z => @@ -63,8 +64,11 @@ object Date { !ttPtr = seconds val tmPtr = alloc[tm] + def getLocalTime() = + if (isWindows) winTime.localtime_s(tmPtr, ttPtr) != 0 + else localtime_r(ttPtr, tmPtr) == null - if (localtime_r(ttPtr, tmPtr) == null) { + if (getLocalTime()) { default } else { // 40 is over-provisioning. @@ -72,9 +76,15 @@ object Date { // + 2 because some IANA timezone abbreviation can have 5 characters. val bufSize = 40.toULong // no toSize_t() yet val buf = alloc[Byte](bufSize) - val n = strftime(buf, bufSize, c"%a %b %d %T %Z %Y", tmPtr) - if (n == 0) default else fromCString(buf) + val n = { + // %Z on Windows might produce long, localized names of variable length + if (isWindows) + winTime.strftime(buf, bufSize, c"%a %b %d %T %Y", tmPtr) + else + strftime(buf, bufSize, c"%a %b %d %T %Z %Y", tmPtr) + } + if (n.toInt == 0) default else fromCString(buf) } } diff --git a/javalib/src/main/scala/java/util/WindowsHelperMethods.scala b/javalib/src/main/scala/java/util/WindowsHelperMethods.scala index 3e958eef12..45228675ab 100644 --- a/javalib/src/main/scala/java/util/WindowsHelperMethods.scala +++ b/javalib/src/main/scala/java/util/WindowsHelperMethods.scala @@ -1,9 +1,12 @@ package java.util -import scala.scalanative.unsafe.stackalloc -import scala.scalanative.windows.HandleApi.Handle +import scala.scalanative.unsafe._ +import java.io.IOException import scala.scalanative.windows.ProcessThreadsApi._ -import scala.scalanative.windows.{DWord, HandleApi} +import scala.scalanative.windows.HandleApi._ +import scala.scalanative.windows.HandleApiExt._ +import scala.scalanative.windows.FileApiExt._ +import scala.scalanative.windows._ private[java] object WindowsHelperMethods { def withUserToken[T](desiredAccess: DWord)(fn: Handle => T): T = { @@ -27,4 +30,31 @@ private[java] object WindowsHelperMethods { throw new RuntimeException("Cannot get user token") } } + + def withFileOpen[T]( + path: String, + access: DWord, + shareMode: DWord = FILE_SHARE_ALL, + disposition: DWord = OPEN_EXISTING, + attributes: DWord = FILE_ATTRIBUTE_NORMAL, + allowInvalidHandle: Boolean = false + )(fn: Handle => T)(implicit z: Zone): T = { + val handle = FileApi.CreateFileW( + toCWideStringUTF16LE(path), + desiredAccess = access, + shareMode = shareMode, + securityAttributes = null, + creationDisposition = disposition, + flagsAndAttributes = attributes, + templateFile = null + ) + if (handle != INVALID_HANDLE_VALUE || allowInvalidHandle) { + try { fn(handle) } + finally CloseHandle(handle) + } else { + throw new IOException( + s"Cannot open file ${path}: ${ErrorHandlingApi.GetLastError()}" + ) + } + } } diff --git a/javalib/src/main/scala/scala/scalanative/nio/fs/FileHelpers.scala b/javalib/src/main/scala/scala/scalanative/nio/fs/FileHelpers.scala index aebdcfe492..4ce0c10bec 100644 --- a/javalib/src/main/scala/scala/scalanative/nio/fs/FileHelpers.scala +++ b/javalib/src/main/scala/scala/scalanative/nio/fs/FileHelpers.scala @@ -1,13 +1,24 @@ package scala.scalanative.nio.fs -import scalanative.unsafe._ import scalanative.libc._ import scalanative.posix.dirent._ -import scalanative.posix.{errno => e, fcntl, unistd}, e._, unistd.access -import scalanative.unsafe._, stdlib._, stdio._, string._ +import scalanative.posix.unistd +import unistd.access +import scalanative.unsafe._ +import stdlib._ +import stdio._ +import scalanative.meta.LinktimeInfo.isWindows import scala.collection.mutable.UnrolledBuffer import scala.reflect.ClassTag import java.io.{File, IOException} +import java.nio.charset.StandardCharsets +import scala.scalanative.windows.{ErrorCodes, WChar} +import scala.scalanative.windows.ErrorHandlingApi._ +import scala.scalanative.windows.FileApi._ +import scala.scalanative.windows.FileApiExt._ +import scala.scalanative.windows.HandleApiExt._ +import scala.scalanative.windows.winnt.AccessRights._ +import scala.scalanative.windows._ object FileHelpers { private[this] lazy val random = new scala.util.Random() @@ -51,11 +62,28 @@ object FileHelpers { false } else Zone { implicit z => - fopen(toCString(path), c"w") match { - case null => - if (throwOnError) throw UnixException(path, errno.errno) - else false - case fd => fclose(fd); exists(path) + if (isWindows) { + val handle = CreateFileW( + toCWideStringUTF16LE(path), + desiredAccess = FILE_GENERIC_WRITE, + shareMode = FILE_SHARE_ALL, + securityAttributes = null, + creationDisposition = CREATE_NEW, + flagsAndAttributes = FILE_ATTRIBUTE_NORMAL, + templateFile = null + ) + HandleApi.CloseHandle(handle) + GetLastError() match { + case ErrorCodes.ERROR_FILE_EXISTS => false + case _ => handle != INVALID_HANDLE_VALUE + } + } else { + fopen(toCString(path), c"w") match { + case null => + if (throwOnError) throw UnixException(path, errno.errno) + else false + case fd => fclose(fd); exists(path) + } } } @@ -70,7 +98,7 @@ object FileHelpers { else if (minLength && prefix.length < 3) throw new IllegalArgumentException("Prefix string too short") else { - val tmpDir = Option(dir).fold(tempDir())(_.toString) + val tmpDir = Option(dir).fold(tempDir)(_.toString) val newSuffix = Option(suffix).getOrElse(".tmp") var result: File = null do { @@ -80,16 +108,40 @@ object FileHelpers { } def exists(path: String): Boolean = - Zone { implicit z => access(toCString(path), unistd.F_OK) == 0 } - private def tempDir(): String = { - val dir = getenv(c"TMPDIR") - if (dir == null) { - System.getProperty("java.io.tmpdir") match { - case null => "/tmp" - case d => d - } + Zone { implicit z => + if (isWindows) { + import ErrorCodes._ + def canAccessAttributes = // fast-path + GetFileAttributesW( + toCWideStringUTF16LE(path) + ) != INVALID_FILE_ATTRIBUTES + def errorCodeIndicatesExistence = GetLastError() match { + case ERROR_FILE_NOT_FOUND | ERROR_PATH_NOT_FOUND | + ERROR_INVALID_NAME => + false + case _ => + true // any other error code indicates that given path might exist + } + canAccessAttributes || errorCodeIndicatesExistence + } else + access(toCString(path), unistd.F_OK) == 0 + } + + lazy val tempDir: String = { + if (isWindows) { + val buffer = stackalloc[WChar](MAX_PATH) + GetTempPathW(MAX_PATH, buffer) + fromCWideString(buffer, StandardCharsets.UTF_16LE) } else { - fromCString(dir) + val dir = getenv(c"TMPDIR") + if (dir == null) { + System.getProperty("java.io.tmpdir") match { + case null => "/tmp" + case d => d + } + } else { + fromCString(dir) + } } } diff --git a/tools/src/main/scala/scala/scalanative/build/LLVM.scala b/tools/src/main/scala/scala/scalanative/build/LLVM.scala index 9f4911a87d..e7c791e97f 100644 --- a/tools/src/main/scala/scala/scalanative/build/LLVM.scala +++ b/tools/src/main/scala/scala/scalanative/build/LLVM.scala @@ -50,8 +50,12 @@ private[scalanative] object LLVM { else Seq("-std=c++11") } else Seq("-std=gnu11") } - val flags = opt(config) +: stdflag ++: "-fvisibility=hidden" +: - config.compileOptions + val platformFlags = { + if (config.targetsWindows) Seq("-g") + else Nil + } + val flags = opt(config) +: "-fvisibility=hidden" +: + stdflag ++: platformFlags ++: config.compileOptions val compilec = Seq(compiler) ++ flto(config) ++ flags ++ target(config) ++ Seq("-c", inpath, "-o", outpath) @@ -103,7 +107,7 @@ private[scalanative] object LLVM { val linkopts = config.linkingOptions ++ links.map("-l" + _) val flags = { val platformFlags = - if (config.targetsWindows) Seq() + if (config.targetsWindows) Seq("-g") else Seq("-rdynamic") flto(config) ++ platformFlags ++ Seq("-o", outpath.abs) ++ target(config) } diff --git a/windowslib/src/main/resources/scala-native/windows/time.c b/windowslib/src/main/resources/scala-native/windows/time.c new file mode 100644 index 0000000000..17a2ff7fb9 --- /dev/null +++ b/windowslib/src/main/resources/scala-native/windows/time.c @@ -0,0 +1,129 @@ +// Windows specific copy of posixlib/time.h +// Uses *_s variants of methods instead of *_r +#if defined(_WIN32) +#define _CRT_SECURE_NO_WARNINGS +#define daylight _daylight +#define timezone _timezone +#define tzname _tzname + +#include +#include + +struct scalanative_tm { + int tm_sec; + int tm_min; + int tm_hour; + int tm_mday; + int tm_mon; + int tm_year; + int tm_wday; + int tm_yday; + int tm_isdst; +}; + +static struct scalanative_tm scalanative_shared_tm_buf; + +static void scalanative_tm_init(struct scalanative_tm *scala_tm, + struct tm *tm) { + scala_tm->tm_sec = tm->tm_sec; + scala_tm->tm_min = tm->tm_min; + scala_tm->tm_hour = tm->tm_hour; + scala_tm->tm_mday = tm->tm_mday; + scala_tm->tm_mon = tm->tm_mon; + scala_tm->tm_year = tm->tm_year; + scala_tm->tm_wday = tm->tm_wday; + scala_tm->tm_yday = tm->tm_yday; + scala_tm->tm_isdst = tm->tm_isdst; +} + +static void tm_init(struct tm *tm, struct scalanative_tm *scala_tm) { + tm->tm_sec = scala_tm->tm_sec; + tm->tm_min = scala_tm->tm_min; + tm->tm_hour = scala_tm->tm_hour; + tm->tm_mday = scala_tm->tm_mday; + tm->tm_mon = scala_tm->tm_mon; + tm->tm_year = scala_tm->tm_year; + tm->tm_wday = scala_tm->tm_wday; + tm->tm_yday = scala_tm->tm_yday; + tm->tm_isdst = scala_tm->tm_isdst; + // On BSD-like systems or with glibc sizeof(tm) is greater than + // sizeof(scalanative_tm), so contents of rest of tm is left undefined. + // asctime, asctime_r, mktime, gmtime, & gmtime_r are robust to this. + // strftime is _NOT_ and must zero the excess fields itself. +} + +errno_t scalanative_asctime_s(struct scalanative_tm *scala_tm, size_t size, + char *buf) { + struct tm tm; + tm_init(&tm, scala_tm); + return asctime_s(buf, size, &tm); +} + +char *scalanative_asctime(struct scalanative_tm *scala_tm) { + struct tm tm; + tm_init(&tm, scala_tm); + return asctime(&tm); +} + +struct scalanative_tm *scalanative_gmtime_s(const time_t *clock, + struct scalanative_tm *result) { + struct tm tm; + gmtime_s(&tm, clock); + scalanative_tm_init(result, &tm); + return result; +} + +struct scalanative_tm *scalanative_gmtime(const time_t *clock) { + return scalanative_gmtime_s(clock, &scalanative_shared_tm_buf); +} + +struct scalanative_tm *scalanative_localtime_s(const time_t *clock, + struct scalanative_tm *result) { + struct tm tm; + localtime_s(&tm, clock); + scalanative_tm_init(result, &tm); + return result; +} + +struct scalanative_tm *scalanative_localtime(const time_t *clock) { + // Calling localtime() ensures that tzset() has been called. + scalanative_tm_init(&scalanative_shared_tm_buf, localtime(clock)); + return &scalanative_shared_tm_buf; +} + +time_t scalanative_mktime(struct scalanative_tm *result) { + struct tm tm; + tm_init(&tm, result); + return mktime(&tm); +} + +size_t scalanative_strftime(char *buf, size_t maxsize, const char *format, + struct scalanative_tm *scala_tm) { + + // The operating system struct tm can be larger than + // the scalanative tm. On 64 bit GNU or _BSD_SOURCE Linux this + // usually is true and beyond easy control. + // + // Clear any fields not known to scalanative, such as tm_zone, + // so they are zero/NULL, not J-Random garbage. + // strftime() in Scala Native release mode is particularly sensitive + // to garbage beyond the end of the scalanative tm. + + // Initializing all of tm when part of it will be immediately overwritten + // is _slightly_ inefficient but short, simple, and easy to get right. + + struct tm tm = {0}; + tm_init(&tm, scala_tm); + return strftime(buf, maxsize, format, &tm); +} + +char **scalanative_tzname() { return tzname; } + +long scalanative_timezone() { return _timezone; } + +int scalanative_daylight() { return _daylight; } + +// Windows compat +void tzset() { return _tzset(); } + +#endif // defined(_WIN32) diff --git a/windowslib/src/main/resources/scala-native/windows/winSocket.c b/windowslib/src/main/resources/scala-native/windows/winSocket.c new file mode 100644 index 0000000000..0a26785351 --- /dev/null +++ b/windowslib/src/main/resources/scala-native/windows/winSocket.c @@ -0,0 +1,14 @@ +#if defined(_WIN32) || defined(WIN32) +#define WIN32_LEAN_AND_MEAN +#include "Windows.h" +#include + +#pragma comment(lib, "Ws2_32.lib") + +DWORD scalanative_winsock_wsadata_size() { return sizeof(WSADATA); } + +DWORD scalanative_winsocket_fionbio() { return FIONBIO; } + +SOCKET scalanative_winsock_invalid_socket() { return INVALID_SOCKET; } + +#endif // defined(_WIN32) diff --git a/windowslib/src/main/scala/scala/scalanative/windows/FileApi.scala b/windowslib/src/main/scala/scala/scalanative/windows/FileApi.scala index 966afea0a5..71b98f07ee 100644 --- a/windowslib/src/main/scala/scala/scalanative/windows/FileApi.scala +++ b/windowslib/src/main/scala/scala/scalanative/windows/FileApi.scala @@ -27,8 +27,25 @@ object FileApi { flagsAndAttributes: UInt, templateFile: Handle ): Handle = extern - + def DeleteFileA(filename: CString): Boolean = extern + def DeleteFileW(filename: CWString): Boolean = extern def FlushFileBuffers(handle: Handle): Boolean = extern + def GetFileAttributesA(filename: CString): DWord = extern + def GetFileAttributesW(filename: CWString): DWord = extern + def GetFullPathNameA( + filename: CString, + bufferLength: DWord, + buffer: CString, + filePart: Ptr[CString] + ): DWord = extern + + def GetFullPathNameW( + filename: CWString, + bufferLength: DWord, + buffer: CWString, + filePart: Ptr[CWString] + ): DWord = extern + def GetFileSizeEx(file: Handle, fileSize: Ptr[LargeInteger]): Boolean = extern def GetTempPathA(bufferLength: DWord, buffer: CString): DWord = extern def GetTempPathW(bufferLength: DWord, buffer: CWString): DWord = extern def ReadFile( @@ -38,7 +55,10 @@ object FileApi { bytesReadPtr: Ptr[DWord], overlapped: Ptr[Byte] ): Boolean = extern - + def SetFileAttributesA(filename: CString, fileAttributes: DWord): Boolean = + extern + def SetFileAttributesW(filename: CWString, fileAttributes: DWord): Boolean = + extern def SetFilePointerEx( file: Handle, distanceToMove: LargeInteger, diff --git a/windowslib/src/main/scala/scala/scalanative/windows/HandleApi.scala b/windowslib/src/main/scala/scala/scalanative/windows/HandleApi.scala index 5faea69697..b8fdfd82ae 100644 --- a/windowslib/src/main/scala/scala/scalanative/windows/HandleApi.scala +++ b/windowslib/src/main/scala/scala/scalanative/windows/HandleApi.scala @@ -1,7 +1,7 @@ package scala.scalanative.windows import scala.scalanative.runtime.fromRawPtr -import scala.scalanative.runtime.Intrinsics.castIntToRawPtr +import scala.scalanative.runtime.Intrinsics.{castIntToRawPtr, castLongToRawPtr} import scala.scalanative.unsafe._ import scala.scalanative.unsigned._ import scala.scalanative.windows.HandleApi.Handle @@ -19,7 +19,7 @@ object HandleApi { } object HandleApiExt { - final val INVALID_HANDLE_VALUE: Handle = fromRawPtr[Byte](castIntToRawPtr(-1)) + final val INVALID_HANDLE_VALUE: Handle = fromRawPtr(castLongToRawPtr(-1)) final val HANDLE_FLAG_INHERIT = 0x00000001.toUInt final val HANDLE_FLAG_PROTECT_FROM_CLOSE = 0x00000002.toUInt } diff --git a/windowslib/src/main/scala/scala/scalanative/windows/WinSocketApi.scala b/windowslib/src/main/scala/scala/scalanative/windows/WinSocketApi.scala new file mode 100644 index 0000000000..68087d2517 --- /dev/null +++ b/windowslib/src/main/scala/scala/scalanative/windows/WinSocketApi.scala @@ -0,0 +1,114 @@ +package scala.scalanative.windows + +import scala.scalanative.unsafe._ +import scalanative.windows.{Word => WinWord} + +@link("ws2_32") +@extern +object WinSocketApi { + + type Socket = Ptr[Byte] + type Group = DWord + type WSAProtocolInfoW = Ptr[Byte] + type WSAPollFd = CStruct3[Socket, CShort, CShort] + // This structures contains additional 5 fields with different order in Win_64 and others + // Should only be treated as read-only and never allocated in ScalaNative. + type WSAData = CStruct2[WinWord, WinWord] + + def WSAStartup(versionRequested: WinWord, data: Ptr[WSAData]): CInt = extern + + def WSACleanup(): CInt = extern + + def WSASocketW( + addressFamily: CInt, + socketType: CInt, + protocol: CInt, + protocolInfo: Ptr[WSAProtocolInfoW], + group: Group, + flags: DWord + ): Socket = extern + + def WSAPoll( + fds: Ptr[WSAPollFd], + nfds: CUnsignedLongInt, + timeout: CInt + ): CInt = + extern + + def WSAGetLastError(): CInt = extern + + @name("ioctlsocket") + def ioctlSocket(socket: Socket, cmd: CInt, argp: Ptr[CInt]): CInt = extern + + @name("closesocket") + def closeSocket(socket: Socket): CInt = extern + + @name("scalanative_winsocket_fionbio") + final def FIONBIO: CInt = extern + + @name("scalanative_winsock_wsadata_size") + final def WSADataSize: CSize = extern + + @name("scalanative_winsock_invalid_socket") + final def InvalidSocket: Socket = extern +} + +object WinSocketApiExt { + final val WinSocketVersion: (Byte, Byte) = (2, 2) + + /* Event flag definitions for WSAPoll(). Their values might differ from Posix constants */ + final val POLLRDNORM = 0x0100 + final val POLLRDBAND = 0x0200 + final val POLLIN = POLLRDNORM | POLLRDBAND + final val POLLPRI = 0x0400 + + final val POLLWRNORM = 0x0010 + final val POLLOUT = POLLWRNORM + final val POLLWRBAND = 0x0020 + + final val POLLERR = 0x0001 + final val POLLHUP = 0x0002 + final val POLLNVAL = 0x0004 + +} + +object WinSocketApiOps { + import WinSocketApi._ + import WinSocketApiExt._ + import util.Conversion._ + + final def init(): Unit = { + val requiredVersion = (wordFromBytes _).tupled(WinSocketVersion) + val winSocketData = stackalloc[Byte](WinSocketApi.WSADataSize) + .asInstanceOf[Ptr[WSAData]] + + val initError = WinSocketApi.WSAStartup(requiredVersion, winSocketData) + if (initError != 0) { + throw new RuntimeException( + s"Failed to initialize socket support, error code $initError " + ) + } + val receivedVersion = wordToBytes(winSocketData.version) + if (WinSocketVersion != receivedVersion) { + WinSocketApi.WSACleanup() + throw new RuntimeException( + s"Could not find a usable version of WinSock.dll, expected $WinSocketVersion, got $receivedVersion" + ) + } + } + + implicit class WSADataOps(val ref: Ptr[WSAData]) extends AnyVal { + def version: WinWord = ref._1 + def highVersion: WinWord = ref._2 + } + + implicit class WSAPollFdOps(val ref: Ptr[WSAPollFd]) extends AnyVal { + def socket: Socket = ref._1 + def events: CShort = ref._2 + def revents: CShort = ref._3 + + def socket_=(v: Socket): Unit = ref._1 = v + def events_=(v: CShort): Unit = ref._2 = v + def revents_=(v: CShort): Unit = ref._3 = v + } +} diff --git a/windowslib/src/main/scala/scala/scalanative/windows/crt/time.scala b/windowslib/src/main/scala/scala/scalanative/windows/crt/time.scala new file mode 100644 index 0000000000..25c9ef2d3d --- /dev/null +++ b/windowslib/src/main/scala/scala/scalanative/windows/crt/time.scala @@ -0,0 +1,50 @@ +package scala.scalanative.windows.crt + +import scala.scalanative.unsafe._ + +@extern +object time { + /* Bindings for time.h which are not part of POSIX standard. + * We assume the same structure as in the POSIX, however on Windows + * we don't need to introduce tm coping glue layer + * For compat with existing POSIX structures we use 32bit variant of methods whenever possible + */ + type clock_t = CLong + type time_t = CLong + type uid_t = CUnsignedInt + type tm = CStruct9[CInt, CInt, CInt, CInt, CInt, CInt, CInt, CInt, CInt] + type errno_t = CInt + + def asctime(time_ptr: Ptr[tm]): CString = extern + def asctime_s(time_ptr: Ptr[tm], size: CSize, buf: Ptr[CChar]): errno_t = + extern + @name("_gmtime32") + def gmtime(time: Ptr[time_t]): Ptr[tm] = extern + @name("_gmtime32_s") + def gmtime_s(tm: Ptr[tm], time: Ptr[time_t]): errno_t = extern + @name("_localtime32") + def localtime(time: Ptr[time_t]): Ptr[tm] = extern + @name("_localtime32_s") + def localtime_s(tm: Ptr[tm], time: Ptr[time_t]): errno_t = extern + + def strftime( + str: Ptr[CChar], + count: CSize, + format: CString, + time: Ptr[tm] + ): CSize = extern + + @name("_time32") + def time(arg: Ptr[time_t]): time_t = extern + @name("_tzset") + def tzset(): Unit = extern + + @name("_daylight") + def daylight(): CInt = extern + + @name("_timezone") + def timezone(): CLong = extern + + @name("_tzname") + def tzname(): Ptr[CStruct2[CString, CString]] = extern +} diff --git a/windowslib/src/main/scala/scala/scalanative/windows/util/Conversion.scala b/windowslib/src/main/scala/scala/scalanative/windows/util/Conversion.scala new file mode 100644 index 0000000000..d6a9b64532 --- /dev/null +++ b/windowslib/src/main/scala/scala/scalanative/windows/util/Conversion.scala @@ -0,0 +1,16 @@ +package scala.scalanative.windows.util + +import scala.scalanative.unsigned._ +import scala.scalanative.windows.{Word => WinWord} + +private[windows] object Conversion { + def wordToBytes(word: WinWord): (Byte, Byte) = { + val lowByte = (word.toInt & 0xff).toByte + val highByte = ((word.toInt >> 8) & 0xff).toByte + (lowByte, highByte) + } + + def wordFromBytes(low: Byte, high: Byte): WinWord = { + ((low & 0xff) | ((high & 0xff) << 8)).toUShort + } +}