Skip to content

Commit

Permalink
Allow to pass Scala Native c interop file as an Input
Browse files Browse the repository at this point in the history
This should provide simpler interop functionality, removing to rely on
passing c files through resource directories.
A new resource caching mechanizm was also added, to smoothen the
development experience. Before, it would be easy for SN linking errors
to appear - any rename or move of an interop file would cause it to be
duplicated. The mappingremoves those issues, as long as users will not
tamper with the newly added file (.project_native_resources) to output
directory. This can be extended for use with regular JVM resources, to
solve a similar issue.
  • Loading branch information
jchyb authored and alexarchambault committed Aug 27, 2022
1 parent 9430de7 commit 770a0d3
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 1 deletion.
8 changes: 8 additions & 0 deletions modules/build/src/main/scala/scala/build/Inputs.scala
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,10 @@ object Inputs {
extends OnDisk with SourceFile with Compiled {
lazy val path: os.Path = base / subPath
}
final case class CFile(base: os.Path, subPath: os.SubPath)
extends OnDisk with SourceFile with Compiled {
lazy val path = base / subPath
}
final case class MarkdownFile(base: os.Path, subPath: os.SubPath)
extends OnDisk with SourceFile {
lazy val path: os.Path = base / subPath
Expand Down Expand Up @@ -232,6 +236,8 @@ object Inputs {
Inputs.ScalaFile(d.path, p.subRelativeTo(d.path))
case p if p.last.endsWith(".sc") =>
Inputs.Script(d.path, p.subRelativeTo(d.path))
case p if p.last.endsWith(".c") || p.last.endsWith(".h") =>
Inputs.CFile(d.path, p.subRelativeTo(d.path))
case p if p.last.endsWith(".md") && enableMarkdown =>
Inputs.MarkdownFile(d.path, p.subRelativeTo(d.path))
}
Expand All @@ -248,6 +254,7 @@ object Inputs {
case _: Inputs.ResourceDirectory => "resource-dir:"
case _: Inputs.JavaFile => "java:"
case _: Inputs.ScalaFile => "scala:"
case _: Inputs.CFile => "c:"
case _: Inputs.Script => "sc:"
case _: Inputs.MarkdownFile => "md:"
}
Expand Down Expand Up @@ -418,6 +425,7 @@ object Inputs {
else if (arg.endsWith(".sc")) Right(Seq(Script(dir, subPath)))
else if (arg.endsWith(".scala")) Right(Seq(ScalaFile(dir, subPath)))
else if (arg.endsWith(".java")) Right(Seq(JavaFile(dir, subPath)))
else if (arg.endsWith(".c") || arg.endsWith(".h")) Right(Seq(CFile(dir, subPath)))
else if (arg.endsWith(".md")) Right(Seq(MarkdownFile(dir, subPath)))
else if (os.isDir(path)) Right(Seq(Directory(path)))
else if (acceptFds && arg.startsWith("/dev/fd/")) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package scala.build.internal.resource

import scala.build.{Build, Inputs}

object NativeResourceMapper extends BaseResourceMapper {

private def scalaNativeCFileMapping(build: Build.Successful): Map[os.Path, os.Path] =
build.inputs.flattened().collect {
case cfile: Inputs.CFile =>
val inputPath = cfile.path
val destPath = build.output / "scala-native" / cfile.subPath
(inputPath, destPath)
}.toMap

private def resolveProjectCFileRegistryPath(nativeWorkDir: os.Path) =
nativeWorkDir / ".native_registry"

/** Copies and maps c file resources from their original path to the destination path in build
* output, also caching output paths in a file.
*
* Remembering the mapping this way allows for the resource to be removed if the original file is
* renamed/deleted.
*/
def copyCFilesToScalaNativeDir(build: Build.Successful, nativeWorkDir: os.Path): Unit = {
val mappingFilePath = resolveProjectCFileRegistryPath(nativeWorkDir)
copyResourcesToDirWithMapping(build.output, mappingFilePath, scalaNativeCFileMapping(build))
}
}
5 changes: 4 additions & 1 deletion modules/cli/src/main/scala/scala/cli/commands/Package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import scala.build.errors.{
import scala.build.interactive.InteractiveFileOps
import scala.build.internal.Util._
import scala.build.internal.{Runner, ScalaJsLinkerConfig}
import scala.build.internal.resource.NativeResourceMapper
import scala.build.options.{PackageType, Platform}
import scala.cli.CurrentParams
import scala.cli.commands.OptionsHelper._
Expand Down Expand Up @@ -928,7 +929,8 @@ object Package extends ScalaCommand[PackageOptions] {
nativeWorkDir
)

if (cacheData.changed)
if (cacheData.changed) {
NativeResourceMapper.copyCFilesToScalaNativeDir(build, nativeWorkDir)
Library.withLibraryJar(build, dest.last.stripSuffix(".jar")) { mainJar =>

val classpath = mainJar.toString +: build.artifacts.classPath.map(_.toString)
Expand Down Expand Up @@ -964,5 +966,6 @@ object Package extends ScalaCommand[PackageOptions] {
else
throw new ScalaNativeBuildError
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,59 @@ abstract class RunTestDefinitions(val scalaVersionOpt: Option[String])
}
}

test("Scala Native C Files are correctly handled as a regular Input") {
val projectDir = "native-interop"
val interopFileName = "bindings.c"
val interopMsg = "Hello C!"
val inputs = TestInputs(
os.rel / projectDir / "main.scala" ->
s"""|//> using platform "scala-native"
|
|import scala.scalanative.unsafe._
|
|@extern
|object Bindings {
| @name("scalanative_print")
| def print(): Unit = extern
|}
|
|object Main {
| def main(args: Array[String]): Unit = {
| Bindings.print()
| }
|}
|""".stripMargin,
os.rel / projectDir / interopFileName ->
s"""|#include <stdio.h>
|
|void scalanative_print() {
| printf("$interopMsg\\n");
|}
|""".stripMargin
)
inputs.fromRoot { root =>
val output =
os.proc(TestUtil.cli, extraOptions, projectDir, "-q")
.call(cwd = root)
.out.text().trim
expect(output == interopMsg)

os.move(root / projectDir / interopFileName, root / projectDir / "bindings2.c")
val output2 =
os.proc(TestUtil.cli, extraOptions, projectDir, "-q")
.call(cwd = root)
.out.text().trim

// LLVM throws linking errors if scalanative_print is internally repeated.
// This can happen if a file containing it will be removed/renamed in src,
// but somehow those changes will not be reflected in the output directory,
// causing symbols inside linked files to be doubled.
// Because of that, the removed file should not be passed to linker,
// otherwise this test will fail.
expect(output2 == interopMsg)
}
}

if (actualScalaVersion.startsWith("3.1"))
test("Scala 3 in Scala Native") {
val message = "using Scala 3 Native"
Expand Down

0 comments on commit 770a0d3

Please sign in to comment.