Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Detect at linktime usage of unsupported features #3472

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 2 additions & 4 deletions javalib/src/main/scala/java/lang/Thread.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import scala.scalanative.runtime.NativeThread.{State => _, _}
import scala.scalanative.runtime.NativeThread.State._
import scala.scalanative.libc.atomic.{CAtomicLongLong, atomic_thread_fence}
import scala.scalanative.libc.atomic.memory_order._
import scala.scalanative.runtime.UnsupportedFeature

import scala.scalanative.runtime.JoinNonDaemonThreads

Expand Down Expand Up @@ -247,10 +248,7 @@ class Thread private[lang] (
}

def start(): Unit = synchronized {
if (!isMultithreadingEnabled)
throw new IllegalStateException(
"ScalaNative application linked with disabled multithreading support"
)
if (!isMultithreadingEnabled) UnsupportedFeature.threads()
if (isVirtual())
throw new UnsupportedOperationException(
"VirtualThreads are not yet supported"
Expand Down
6 changes: 3 additions & 3 deletions javalib/src/main/scala/java/lang/VirtualThread.scala
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
package java.lang

import scala.scalanative.runtime.UnsupportedFeature

final private[lang] class VirtualThread(
name: String,
characteristics: Int,
task: Runnable
) extends Thread(name, characteristics) {

// TODO: continuations-based thread implementation
override def run(): Unit = throw new UnsupportedOperationException(
"Running VirtualThreads is not yet supported"
)
override def run(): Unit = UnsupportedFeature.virtualThreads()

override def getState(): Thread.State = Thread.State.NEW
}
4 changes: 2 additions & 2 deletions javalib/src/main/scala/java/lang/impl/PosixThread.scala
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ private[java] class PosixThread(val thread: Thread, stackSize: Long)

private val handle: pthread_t =
if (isMainThread) 0.toUSize // main thread
else if (!isMultithreadingEnabled)
else if (!isMultithreadingEnabled) {
throw new LinkageError(
"Multithreading support disabled - cannot create new threads"
)
else {
} else {
val id = stackalloc[pthread_t]()
val attrs = stackalloc[Byte](pthread_attr_t_size)
.asInstanceOf[Ptr[pthread_attr_t]]
Expand Down
4 changes: 2 additions & 2 deletions javalib/src/main/scala/java/lang/impl/WindowsThread.scala
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@ private[java] class WindowsThread(val thread: Thread, stackSize: Long)

private val handle: Handle = {
if (isMainThread) 0.toPtr // main thread
else if (!isMultithreadingEnabled)
else if (!isMultithreadingEnabled) {
throw new LinkageError(
"Multithreading support disabled - cannot create new threads"
)
else
} else
checkedHandle("create thread") {
val effectiveStackSize =
if (stackSize > 0) stackSize
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package scala.scalanative.runtime;

/**
* Mock methods used to detect at linktime usage of unsupported features. Use
* with caution and always use guarded with linktime-reasolved conditions. Eg.
*
* <pre>
* {@code
* object MyThread extends Thread(){
* override def run(): Unit =
* if !scala.scalanative.meta.LinktimeInfo.isMultithreadingEnabled
* then UnsupportedFeature.threads() // fail compilation if multithreading is disabled
* else runLogic()
* }
* }
* </pre>
*
* Checking of unsupported features can be disabled in nativeConfig using
* checkFeatures flag
*/
public abstract class UnsupportedFeature {
// Always sync with tools/src/main/scala/scala/scalanative/linker/Reach.scala
// UnsupportedFeature and UnsupportedFeatureExtractor and the stubs
public static void threads() {
}

public static void virtualThreads() {
}
}
4 changes: 4 additions & 0 deletions project/Build.scala
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,8 @@ object Build {
nativeConfig ~= { c =>
c.withLinkStubs(true)
.withEmbedResources(true)
// Tests using threads are ignored in runtime, skip checks and allow to link
.withCheckFeatures(false)
},
Test / unmanagedSourceDirectories ++= {
val base = (Test / sourceDirectory).value
Expand Down Expand Up @@ -970,6 +972,8 @@ object Build {
scalacOptions --= Seq(
"-Xfatal-warnings"
),
// No control over sources
nativeConfig ~= { _.withCheckFeatures(false) },
testOptions += Tests.Argument(TestFrameworks.JUnit, "-a", "-s"),
shouldPartest := {
(Test / resourceDirectory).value / scalaVersion.value
Expand Down
14 changes: 14 additions & 0 deletions tools/src/main/scala/scala/scalanative/build/NativeConfig.scala
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ sealed trait NativeConfig {
/** Shall linker NIR check treat warnings as errors? */
def checkFatalWarnings: Boolean

/** Should build fail if it detects usage of unsupported feature on given
* platform
*/
def checkFeatures: Boolean

/** Shall linker dump intermediate NIR after every phase? */
def dump: Boolean

Expand Down Expand Up @@ -153,6 +158,9 @@ sealed trait NativeConfig {
/** Create a new config with given checkFatalWarnings value. */
def withCheckFatalWarnings(value: Boolean): NativeConfig

/** Create a new config with given checkFeatures value. */
def withCheckFeatures(value: Boolean): NativeConfig

/** Create a new config with given dump value. */
def withDump(value: Boolean): NativeConfig

Expand Down Expand Up @@ -223,6 +231,7 @@ object NativeConfig {
buildTarget = BuildTarget.default,
check = false,
checkFatalWarnings = false,
checkFeatures = true,
dump = false,
asan = false,
linkStubs = false,
Expand All @@ -248,6 +257,7 @@ object NativeConfig {
buildTarget: BuildTarget,
check: Boolean,
checkFatalWarnings: Boolean,
checkFeatures: Boolean,
dump: Boolean,
asan: Boolean,
linkStubs: Boolean,
Expand Down Expand Up @@ -307,6 +317,9 @@ object NativeConfig {
def withCheckFatalWarnings(value: Boolean): NativeConfig =
copy(checkFatalWarnings = value)

def withCheckFeatures(value: Boolean): NativeConfig =
copy(checkFeatures = value)

def withDump(value: Boolean): NativeConfig =
copy(dump = value)

Expand Down Expand Up @@ -374,6 +387,7 @@ object NativeConfig {
| - buildTarget $buildTarget
| - check: $check
| - checkFatalWarnings: $checkFatalWarnings
| - checkFeatures $checkFeatures
| - dump: $dump
| - asan: $asan
| - linkStubs: $linkStubs
Expand Down
62 changes: 43 additions & 19 deletions tools/src/main/scala/scala/scalanative/build/ScalaNative.scala
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ private[scalanative] object ScalaNative {
.flatMap {
case result: ReachabilityAnalysis.Result =>
check(config, forceQuickCheck = forceQuickCheck)(result)
case result: ReachabilityAnalysis.UnreachableSymbolsFound =>
case result: ReachabilityAnalysis.Failure =>
Future.failed(
new LinkingException(
s"Unreachable symbols found after $stage run. It can happen when using dependencies not cross-compiled for Scala Native or not yet ported JDK definitions."
Expand All @@ -103,24 +103,48 @@ private[scalanative] object ScalaNative {
analysis: ReachabilityAnalysis,
stage: String
): Unit = {
def showUnreachable(
analysis: ReachabilityAnalysis.UnreachableSymbolsFound
def showFailureDetails(
analysis: ReachabilityAnalysis.Failure
): Unit = {
val log = config.logger
log.error(s"Found ${analysis.unreachable.size} unreachable symbols")
analysis.unreachable.foreach {
case Reach.UnreachableSymbol(_, kind, name, backtrace) =>
// Build stacktrace in memory to prevent its spliting when logging asynchronously
val buf = new StringBuilder()
buf.append(s"Found unknown $kind $name, referenced from:\n")
val padding = backtrace.foldLeft(0)(_ max _.kind.length())
backtrace.foreach {
case Reach.BackTraceElement(_, kind, name, filename, line) =>
val pad = " " * (padding - kind.length())
buf.append(s" $pad$kind at $name($filename:$line)\n")
}
buf.append("\n")
log.error(buf.toString())
def appendBackTrace(
buf: StringBuilder,
backtrace: List[Reach.BackTraceElement]
): Unit = {
// Build stacktrace in memory to prevent its spliting when logging asynchronously
val padding = backtrace.foldLeft(0)(_ max _.kind.length())
backtrace.foreach {
case Reach.BackTraceElement(_, kind, name, filename, line) =>
val pad = " " * (padding - kind.length())
buf.append(s" $pad$kind at $name($filename:$line)\n")
}
buf.append("\n")
}

if (analysis.unreachable.nonEmpty) {
log.error(s"Found ${analysis.unreachable.size} unreachable symbols!")
analysis.unreachable.foreach {
case Reach.UnreachableSymbol(_, kind, name, backtrace) =>
val buf = new StringBuilder()
buf.append(s"Found unknown $kind $name, referenced from:\n")
appendBackTrace(buf, backtrace)
log.error(buf.toString())
}
}

if (analysis.unsupportedFeatures.nonEmpty) {
log.error(
s"Found usage of ${analysis.unsupportedFeatures.size} unsupported features!"
)
analysis.unsupportedFeatures.foreach {
case Reach.UnsupportedFeature(kind, backtrace) =>
val buf = new StringBuilder()
buf.append(
s"Detected usage of unsupported feature ${kind} - ${kind.details}\nFeature referenced from:\n"
)
appendBackTrace(buf, backtrace)
log.error(buf.toString())
}
}
}

Expand All @@ -136,9 +160,9 @@ private[scalanative] object ScalaNative {
}

analysis match {
case result: ReachabilityAnalysis.UnreachableSymbolsFound =>
case result: ReachabilityAnalysis.Failure =>
showStats()
showUnreachable(result)
showFailureDetails(result)
case _ =>
showStats()
}
Expand Down
5 changes: 3 additions & 2 deletions tools/src/main/scala/scala/scalanative/linker/Infos.scala
Original file line number Diff line number Diff line change
Expand Up @@ -230,9 +230,10 @@ sealed trait ReachabilityAnalysis {
}

object ReachabilityAnalysis {
final class UnreachableSymbolsFound(
final class Failure(
val defns: Seq[Defn],
val unreachable: Seq[Reach.UnreachableSymbol]
val unreachable: Seq[Reach.UnreachableSymbol],
val unsupportedFeatures: Seq[Reach.UnsupportedFeature]
) extends ReachabilityAnalysis
final class Result(
val infos: mutable.Map[Global, Info],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ trait LinktimeValueResolver { self: Reach =>
case Inst.Let(_, op, Next.None) =>
op match {
case Op.Call(_, Val.Global(name, _), _) =>
track(name)(inst.pos)
name != Linktime.PropertyResolveFunctionName &&
!lookup(name).exists(_.attrs.isLinktimeResolved)
case _: Op.Comp => false
Expand Down Expand Up @@ -247,7 +248,7 @@ trait LinktimeValueResolver { self: Reach =>
case Next.Label(_, values) =>
locals ++= nextBlock.params.zip(values).toMap
case _ =>
unsupported(
scalanative.util.unsupported(
"Only normal labels are expected in linktime resolved methods"
)
}
Expand All @@ -263,7 +264,7 @@ trait LinktimeValueResolver { self: Reach =>

case _: Inst.If | _: Inst.Let | _: Inst.Switch | _: Inst.Throw |
_: Inst.Unreachable =>
unsupported(
scalanative.util.unsupported(
"Unexpected instruction found in linktime resolved method: " + inst
)
}
Expand Down