Skip to content

Commit

Permalink
Allow to run unit-tests infrastructure on Windows (scala-native#2287)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
WojciechMazur committed Aug 25, 2021
1 parent 07c3673 commit 994bbc9
Show file tree
Hide file tree
Showing 23 changed files with 946 additions and 252 deletions.
11 changes: 7 additions & 4 deletions .github/workflows/run-tests-windows.yml
Expand Up @@ -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
Expand Down Expand Up @@ -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
165 changes: 124 additions & 41 deletions javalib/src/main/scala/java/io/File.scala
Expand Up @@ -2,26 +2,34 @@ 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._

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(
Expand All @@ -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())
Expand Down Expand Up @@ -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)
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 =>
Expand All @@ -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")
Expand All @@ -231,22 +263,45 @@ 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
} else {
0L
}
}
}

def list(): Array[String] =
list(FilenameFilter.allPassFilter)
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
}

Expand All @@ -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())
Expand All @@ -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)
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 =
Expand All @@ -575,6 +657,7 @@ object File {
} else if (uri.getRawFragment() != null) {
throwExc(compMsg("fragment"))
}
uri
// else URI is ok
}
}
3 changes: 2 additions & 1 deletion javalib/src/main/scala/java/io/FileOutputStream.scala
Expand Up @@ -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)
Expand Down
6 changes: 5 additions & 1 deletion javalib/src/main/scala/java/lang/StackTraceElement.scala
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 994bbc9

Please sign in to comment.