Skip to content

Commit

Permalink
refactor: C library bindings should be able to be made against c file…
Browse files Browse the repository at this point in the history
…s in resources (#128)

Fixes #89
  • Loading branch information
markehammons authored Apr 2, 2023
1 parent a407850 commit fcb054c
Show file tree
Hide file tree
Showing 11 changed files with 134 additions and 47 deletions.
2 changes: 1 addition & 1 deletion core/src/fr/hammons/slinc/FSet.scala
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ object FSet:
'{
new FSet[L]:
val dependencies =
NeedsResource[L].map(nr => Dependency.Resource(nr.resourcePath))
NeedsResource[L].map(_.toDependency)
val description = $descriptors
val generation = $generators
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package fr.hammons.slinc.annotations

import fr.hammons.slinc.fset.Dependency

trait DependencyAnnotation:
def toDependency: Dependency
8 changes: 7 additions & 1 deletion core/src/fr/hammons/slinc/annotations/NeedsResource.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,15 @@ package fr.hammons.slinc.annotations
import scala.annotation.StaticAnnotation

import quoted.*
import fr.hammons.slinc.fset.Dependency
import java.nio.file.Paths

final case class NeedsResource(val resourcePath: String)
extends StaticAnnotation
extends StaticAnnotation,
DependencyAnnotation:
def toDependency: Dependency = resourcePath match
case path @ s"${_}.c" => Dependency.CResource(Paths.get(path).nn)
case path => Dependency.LibraryResource(Paths.get(path).nn)

object NeedsResource:
inline def apply[L]: List[NeedsResource] = ${
Expand Down
5 changes: 4 additions & 1 deletion core/src/fr/hammons/slinc/fset/Dependency.scala
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
package fr.hammons.slinc.fset

import java.nio.file.Path

enum Dependency:
case Resource(path: String)
case LibraryResource(path: Path)
case CResource(path: Path)
5 changes: 5 additions & 0 deletions core/src/fr/hammons/slinc/modules/CacheFile.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package fr.hammons.slinc.modules

import java.nio.file.Path

final case class CacheFile(origin: Path, cachePath: Path, updated: Boolean)
65 changes: 60 additions & 5 deletions core/src/fr/hammons/slinc/modules/LinkageTools.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,24 @@ import java.nio.file.Paths
import fr.hammons.slinc.types.{OS, os}
import java.nio.file.Files
import java.nio.file.Path
import java.io.InputStream
import java.security.MessageDigest
import java.io.FileInputStream
import fr.hammons.slinc.fset.Dependency
import java.util.concurrent.atomic.AtomicReference
import java.io.InputStream

import scala.sys.process.*

object LinkageTools:
private val dependenciesLoaded = AtomicReference(Set.empty[Dependency])

private val libSuffix = os match
case OS.Linux | OS.Darwin => ".so"
case OS.Windows => ".dll"
case OS.Unknown => throw Error("Lib suffix is unknown on this platform")

private val resourcesLocation = "/native"

private val md =
ThreadLocal.withInitial(() => MessageDigest.getInstance("SHA256")).nn
private val cacheLocation = os match
Expand All @@ -20,13 +33,15 @@ object LinkageTools:
Paths
.get(System.getProperty("user.home"), "Library", "Application Support")
.nn
case OS.Unknown => ???
case OS.Unknown => throw Error("Cache location is unknown on this platform")

def sendResourceToCache(location: String): String =
def sendResourceToCache(location: Path): CacheFile =
if !Files.exists(cacheLocation) then Files.createDirectories(cacheLocation)
val cacheFile = cacheLocation.resolve(location)
val resourceHash = hash(
getClass().getResourceAsStream(s"/native/$location").nn
getClass()
.getResourceAsStream(s"$resourcesLocation/$location")
.nn
)

if !Files.exists(cacheFile) || hash(
Expand All @@ -38,11 +53,51 @@ object LinkageTools:
if stream != null then Files.copy(stream, cacheFile)
else throw Error(s"Could not find resource /native/$location")

cacheFile.toString()
CacheFile(location, cacheFile.nn, true)
else CacheFile(location, cacheFile.nn, false)

def load(path: Path): Unit =
System.load(path.toAbsolutePath().toString())

def loadDependency(dependency: Dependency): Unit = synchronized {
val currentDeps = dependenciesLoaded.get.nn
if !currentDeps.contains(dependency) then
dependency match
case Dependency.LibraryResource(path) =>
val cachedFile = sendResourceToCache(path)
load(cachedFile.cachePath)
case Dependency.CResource(path) =>
val cachedFile = sendResourceToCache(path)
val compilationPath = compileCachedCCode(cachedFile)
load(compilationPath)

dependenciesLoaded.compareAndExchange(
currentDeps,
currentDeps + dependency
)
}

def compileCachedCCode(cachedFile: CacheFile): Path =
val libLocation = cachedFile.cachePath.toString() match
case s"${baseName}.c" => Paths.get(s"$baseName$libSuffix").nn

if !Files.exists(libLocation) || cachedFile.updated then
val cmd = Seq(
"clang",
"-shared",
"-fvisibility=default",
"-Os",
"-o",
libLocation.nn.toAbsolutePath().nn.toString(),
cachedFile.cachePath.toAbsolutePath().nn.toString()
)
if cmd.! != 0 then
throw Error(
s"failed to compile resource ${cachedFile.origin.toAbsolutePath()}"
)

libLocation

def hash(stream: InputStream): Seq[Byte] =
val buffer = Array.ofDim[Byte](1024)
var len = stream.read(buffer)
Expand Down
12 changes: 12 additions & 0 deletions core/test/src/fr/hammons/slinc/FSetRuntimeSpec.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package fr.hammons.slinc

import java.{util as ju}
import fr.hammons.slinc.annotations.NeedsResource
import fr.hammons.slinc.types.CInt

trait FSetRuntimeSpec(val slinc: Slinc) extends munit.FunSuite:
import slinc.given
Expand All @@ -22,3 +24,13 @@ trait FSetRuntimeSpec(val slinc: Slinc) extends munit.FunSuite:
summon[FSet[L]].description.head
).returnAllocates
)

test("C resource loading works"):
@NeedsResource("test.c")
trait L derives FSet:
def identity_int(i: CInt): CInt

assertEquals(
FSet.instance[L].identity_int(5),
5
)
15 changes: 13 additions & 2 deletions core/test/src/fr/hammons/slinc/FSetSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import fr.hammons.slinc.types.{CInt, TimeT, CChar}
import fr.hammons.slinc.types.{OS, Arch}
import fr.hammons.slinc.annotations.*
import fr.hammons.slinc.fset.Dependency
import java.nio.file.Paths

class FSetSpec extends munit.FunSuite:
test("Unit parameters don't compile"):
Expand Down Expand Up @@ -91,12 +92,22 @@ class FSetSpec extends munit.FunSuite:
)
)

test("resources should be recorded for loading"):
test("library resources should be recorded for loading"):
@NeedsResource("test")
trait L derives FSet:
def abs(i: CInt): CInt

assertEquals(
summon[FSet[L]].dependencies,
List(Dependency.Resource("test"))
List(Dependency.LibraryResource(Paths.get("test").nn))
)

test("c resources should be recorded for loading"):
@NeedsResource("test.c")
trait L derives FSet:
def abs(i: CInt): CInt

assertEquals(
summon[FSet[L]].dependencies,
List(Dependency.CResource(Paths.get("test.c").nn))
)
29 changes: 18 additions & 11 deletions core/test/src/fr/hammons/slinc/modules/LinkageToolsSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,36 @@ import java.io.FileInputStream

class LinkageToolsSpec extends munit.FunSuite:
test("send resource to cache"):
val resultLocation = LinkageTools.sendResourceToCache("test.c")
val resultPath = Paths.get(resultLocation)
val testC = Paths.get("test.c").nn
val resultLocation =
LinkageTools.sendResourceToCache(testC)

assert(Files.exists(resultPath))
Files.delete(resultPath)
assert(Files.exists(resultLocation.cachePath))
Files.delete(resultLocation.cachePath)

LinkageTools.sendResourceToCache("test.c")
assert(Files.exists(resultPath))
LinkageTools.sendResourceToCache(testC)
assert(Files.exists(resultLocation.cachePath))

Files.writeString(resultPath, "lala")
Files.writeString(resultLocation.cachePath, "lala")

assertEquals(Files.readString(resultPath), "lala")
assertEquals(Files.readString(resultLocation.cachePath), "lala")

LinkageTools.sendResourceToCache("test.c")
val cacheResult = LinkageTools.sendResourceToCache(testC)

assertNotEquals(Files.readString(resultPath), "lala")
assertNotEquals(Files.readString(resultLocation.cachePath), "lala")
assert(cacheResult.updated)

test("hashing works"):
val hash1 =
LinkageTools.hash(getClass().getResourceAsStream(s"/native/test.c").nn)

val hash2 = LinkageTools.hash(
FileInputStream(LinkageTools.sendResourceToCache("test.c"))
FileInputStream(
LinkageTools
.sendResourceToCache(Paths.get("test.c").nn)
.cachePath
.toString()
)
)

assertEquals(hash1, hash2)
17 changes: 4 additions & 13 deletions j17/src/fr/hammons/slinc/modules/FSetModule17.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,31 +14,22 @@ given fsetModule17: FSetModule with
val runtimeVersion = 17
import LinkageModule17.*

val loadedLibraries: AtomicReference[Set[Dependency]] =
AtomicReference.apply(Set.empty)

override def getBacking(
dependencies: List[Dependency],
desc: List[CFunctionDescriptor],
generators: List[FunctionBindingGenerator]
): FSetBacking[?] =

val loaded = loadedLibraries.get().nn
val newLoaded = dependencies.foldLeft(loaded):
case (loaded, dep @ Dependency.Resource(path)) if !loaded.contains(dep) =>
System.load(path)
loaded + dep
case (loaded, _) =>
loaded

loadedLibraries.compareAndExchange(loaded, newLoaded)
dependencies.foreach(LinkageTools.loadDependency)

val fns = desc
.zip(generators)
.map:
case (cfd, generator) =>
val functionInformation = FunctionContext(cfd)
val addr = defaultLookup(functionInformation.name).get
val addr = defaultLookup(functionInformation.name)
.orElse(loaderLookup(functionInformation.name))
.get
val mh: MethodHandler = MethodHandler((v: Seq[Variadic]) =>
getDowncall(cfd, v).bindTo(addr).nn
)
Expand Down
17 changes: 4 additions & 13 deletions j19/src/fr/hammons/slinc/modules/FSetModule19.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,23 @@ import fr.hammons.slinc.fset.Dependency
given fsetModule19: FSetModule with
override val runtimeVersion: Int = 19

val loadedLibraries: AtomicReference[Set[Dependency]] =
AtomicReference.apply(Set.empty)

override def getBacking(
dependencies: List[Dependency],
desc: List[CFunctionDescriptor],
generators: List[FunctionBindingGenerator]
): FSetBacking[?] =
import LinkageModule19.*

val loaded = loadedLibraries.get().nn
val newLoaded = dependencies.foldLeft(loaded):
case (loaded, dep @ Dependency.Resource(path)) if !loaded.contains(dep) =>
System.load(path)
loaded + dep
case (loaded, _) =>
loaded

loadedLibraries.compareAndExchange(loaded, newLoaded)
dependencies.foreach(LinkageTools.loadDependency)

val fns = desc
.zip(generators)
.map:
case (cfd, generator) =>
val runtimeInformation = FunctionContext(cfd)
val addr = defaultLookup(runtimeInformation.name).get
val addr = defaultLookup(runtimeInformation.name)
.orElse(loaderLookup(runtimeInformation.name))
.get
val mh: MethodHandler = MethodHandler((v: Seq[Variadic]) =>
getDowncall(cfd, v).bindTo(addr).nn
)
Expand Down

0 comments on commit fcb054c

Please sign in to comment.