Skip to content

Commit 6f25287

Browse files
jchyblwronski
authored andcommitted
Allow to pass Scala Native c interop file as an Input
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.
1 parent 7145b1f commit 6f25287

File tree

4 files changed

+101
-1
lines changed

4 files changed

+101
-1
lines changed

modules/build/src/main/scala/scala/build/Inputs.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,10 @@ object Inputs {
201201
extends OnDisk with SourceFile with Compiled {
202202
lazy val path: os.Path = base / subPath
203203
}
204+
final case class CFile(base: os.Path, subPath: os.SubPath)
205+
extends OnDisk with SourceFile with Compiled {
206+
lazy val path = base / subPath
207+
}
204208
final case class MarkdownFile(base: os.Path, subPath: os.SubPath)
205209
extends OnDisk with SourceFile {
206210
lazy val path: os.Path = base / subPath
@@ -240,6 +244,8 @@ object Inputs {
240244
Inputs.SourceScalaFile(d.path, p.subRelativeTo(d.path))
241245
case p if p.last.endsWith(".sc") =>
242246
Inputs.Script(d.path, p.subRelativeTo(d.path))
247+
case p if p.last.endsWith(".c") || p.last.endsWith(".h") =>
248+
Inputs.CFile(d.path, p.subRelativeTo(d.path))
243249
case p if p.last.endsWith(".md") && enableMarkdown =>
244250
Inputs.MarkdownFile(d.path, p.subRelativeTo(d.path))
245251
}
@@ -269,6 +275,7 @@ object Inputs {
269275
case _: Inputs.JavaFile => "java:"
270276
case _: Inputs.SettingsScalaFile => "config:"
271277
case _: Inputs.SourceScalaFile => "scala:"
278+
case _: Inputs.CFile => "c:"
272279
case _: Inputs.Script => "sc:"
273280
case _: Inputs.MarkdownFile => "md:"
274281
}
@@ -455,6 +462,7 @@ object Inputs {
455462
else if (arg.endsWith(".sc")) Right(Seq(Script(dir, subPath)))
456463
else if (arg.endsWith(".scala")) Right(Seq(SourceScalaFile(dir, subPath)))
457464
else if (arg.endsWith(".java")) Right(Seq(JavaFile(dir, subPath)))
465+
else if (arg.endsWith(".c") || arg.endsWith(".h")) Right(Seq(CFile(dir, subPath)))
458466
else if (arg.endsWith(".md")) Right(Seq(MarkdownFile(dir, subPath)))
459467
else if (os.isDir(path)) Right(Seq(Directory(path)))
460468
else if (acceptFds && arg.startsWith("/dev/fd/")) {
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package scala.build.internal.resource
2+
3+
import scala.build.{Build, Inputs}
4+
5+
object NativeResourceMapper {
6+
7+
private def scalaNativeCFileMapping(build: Build.Successful): Map[os.Path, os.RelPath] =
8+
build
9+
.inputs
10+
.flattened()
11+
.collect {
12+
case cfile: Inputs.CFile =>
13+
val inputPath = cfile.path
14+
val destPath = os.rel / "scala-native" / cfile.subPath
15+
(inputPath, destPath)
16+
}
17+
.toMap
18+
19+
private def resolveProjectCFileRegistryPath(nativeWorkDir: os.Path) =
20+
nativeWorkDir / ".native_registry"
21+
22+
/** Copies and maps c file resources from their original path to the destination path in build
23+
* output, also caching output paths in a file.
24+
*
25+
* Remembering the mapping this way allows for the resource to be removed if the original file is
26+
* renamed/deleted.
27+
*/
28+
def copyCFilesToScalaNativeDir(build: Build.Successful, nativeWorkDir: os.Path): Unit = {
29+
val mappingFilePath = resolveProjectCFileRegistryPath(nativeWorkDir)
30+
ResourceMapper.copyResourcesToDirWithMapping(
31+
build.output,
32+
mappingFilePath,
33+
scalaNativeCFileMapping(build)
34+
)
35+
}
36+
}

modules/cli/src/main/scala/scala/cli/commands/Package.scala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import scala.build.errors.*
2222
import scala.build.interactive.InteractiveFileOps
2323
import scala.build.internal.Util.*
2424
import scala.build.internal.{Runner, ScalaJsLinkerConfig}
25+
import scala.build.internal.resource.NativeResourceMapper
2526
import scala.build.options.{PackageType, Platform}
2627
import scala.cli.CurrentParams
2728
import scala.cli.commands.OptionsHelper.*
@@ -941,7 +942,8 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {
941942
nativeWorkDir
942943
)
943944

944-
if (cacheData.changed)
945+
if (cacheData.changed) {
946+
NativeResourceMapper.copyCFilesToScalaNativeDir(build, nativeWorkDir)
945947
Library.withLibraryJar(build, dest.last.stripSuffix(".jar")) { mainJar =>
946948

947949
val classpath = mainJar.toString +: build.artifacts.classPath.map(_.toString)
@@ -977,5 +979,6 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {
977979
else
978980
throw new ScalaNativeBuildError
979981
}
982+
}
980983
}
981984
}

modules/integration/src/test/scala/scala/cli/integration/RunTestDefinitions.scala

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,59 @@ abstract class RunTestDefinitions(val scalaVersionOpt: Option[String])
292292
}
293293
}
294294

295+
test("Scala Native C Files are correctly handled as a regular Input") {
296+
val projectDir = "native-interop"
297+
val interopFileName = "bindings.c"
298+
val interopMsg = "Hello C!"
299+
val inputs = TestInputs(
300+
os.rel / projectDir / "main.scala" ->
301+
s"""|//> using platform "scala-native"
302+
|
303+
|import scala.scalanative.unsafe._
304+
|
305+
|@extern
306+
|object Bindings {
307+
| @name("scalanative_print")
308+
| def print(): Unit = extern
309+
|}
310+
|
311+
|object Main {
312+
| def main(args: Array[String]): Unit = {
313+
| Bindings.print()
314+
| }
315+
|}
316+
|""".stripMargin,
317+
os.rel / projectDir / interopFileName ->
318+
s"""|#include <stdio.h>
319+
|
320+
|void scalanative_print() {
321+
| printf("$interopMsg\\n");
322+
|}
323+
|""".stripMargin
324+
)
325+
inputs.fromRoot { root =>
326+
val output =
327+
os.proc(TestUtil.cli, extraOptions, projectDir, "-q")
328+
.call(cwd = root)
329+
.out.trim()
330+
expect(output == interopMsg)
331+
332+
os.move(root / projectDir / interopFileName, root / projectDir / "bindings2.c")
333+
val output2 =
334+
os.proc(TestUtil.cli, extraOptions, projectDir, "-q")
335+
.call(cwd = root)
336+
.out.trim()
337+
338+
// LLVM throws linking errors if scalanative_print is internally repeated.
339+
// This can happen if a file containing it will be removed/renamed in src,
340+
// but somehow those changes will not be reflected in the output directory,
341+
// causing symbols inside linked files to be doubled.
342+
// Because of that, the removed file should not be passed to linker,
343+
// otherwise this test will fail.
344+
expect(output2 == interopMsg)
345+
}
346+
}
347+
295348
if (actualScalaVersion.startsWith("3.1"))
296349
test("Scala 3 in Scala Native") {
297350
val message = "using Scala 3 Native"

0 commit comments

Comments
 (0)