diff --git a/core/src/fr/hammons/slinc/FSet.scala b/core/src/fr/hammons/slinc/FSet.scala index 2bf1b7ca..a0628c8e 100644 --- a/core/src/fr/hammons/slinc/FSet.scala +++ b/core/src/fr/hammons/slinc/FSet.scala @@ -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 } diff --git a/core/src/fr/hammons/slinc/annotations/DependencyAnnotation.scala b/core/src/fr/hammons/slinc/annotations/DependencyAnnotation.scala new file mode 100644 index 00000000..0aee3153 --- /dev/null +++ b/core/src/fr/hammons/slinc/annotations/DependencyAnnotation.scala @@ -0,0 +1,6 @@ +package fr.hammons.slinc.annotations + +import fr.hammons.slinc.fset.Dependency + +trait DependencyAnnotation: + def toDependency: Dependency diff --git a/core/src/fr/hammons/slinc/annotations/NeedsResource.scala b/core/src/fr/hammons/slinc/annotations/NeedsResource.scala index c773538e..c2fa81b1 100644 --- a/core/src/fr/hammons/slinc/annotations/NeedsResource.scala +++ b/core/src/fr/hammons/slinc/annotations/NeedsResource.scala @@ -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] = ${ diff --git a/core/src/fr/hammons/slinc/fset/Dependency.scala b/core/src/fr/hammons/slinc/fset/Dependency.scala index bad0707c..0041ed81 100644 --- a/core/src/fr/hammons/slinc/fset/Dependency.scala +++ b/core/src/fr/hammons/slinc/fset/Dependency.scala @@ -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) diff --git a/core/src/fr/hammons/slinc/modules/CacheFile.scala b/core/src/fr/hammons/slinc/modules/CacheFile.scala new file mode 100644 index 00000000..562b7eb3 --- /dev/null +++ b/core/src/fr/hammons/slinc/modules/CacheFile.scala @@ -0,0 +1,5 @@ +package fr.hammons.slinc.modules + +import java.nio.file.Path + +final case class CacheFile(origin: Path, cachePath: Path, updated: Boolean) diff --git a/core/src/fr/hammons/slinc/modules/LinkageTools.scala b/core/src/fr/hammons/slinc/modules/LinkageTools.scala index c6c32112..a9cd2962 100644 --- a/core/src/fr/hammons/slinc/modules/LinkageTools.scala +++ b/core/src/fr/hammons/slinc/modules/LinkageTools.scala @@ -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 @@ -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( @@ -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) diff --git a/core/test/src/fr/hammons/slinc/FSetRuntimeSpec.scala b/core/test/src/fr/hammons/slinc/FSetRuntimeSpec.scala index 8f3005b0..9d433ff2 100644 --- a/core/test/src/fr/hammons/slinc/FSetRuntimeSpec.scala +++ b/core/test/src/fr/hammons/slinc/FSetRuntimeSpec.scala @@ -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 @@ -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 + ) diff --git a/core/test/src/fr/hammons/slinc/FSetSpec.scala b/core/test/src/fr/hammons/slinc/FSetSpec.scala index 16b3d540..8d4311b6 100644 --- a/core/test/src/fr/hammons/slinc/FSetSpec.scala +++ b/core/test/src/fr/hammons/slinc/FSetSpec.scala @@ -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"): @@ -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)) ) diff --git a/core/test/src/fr/hammons/slinc/modules/LinkageToolsSpec.scala b/core/test/src/fr/hammons/slinc/modules/LinkageToolsSpec.scala index 0d7a6029..8f367d94 100644 --- a/core/test/src/fr/hammons/slinc/modules/LinkageToolsSpec.scala +++ b/core/test/src/fr/hammons/slinc/modules/LinkageToolsSpec.scala @@ -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) diff --git a/j17/src/fr/hammons/slinc/modules/FSetModule17.scala b/j17/src/fr/hammons/slinc/modules/FSetModule17.scala index ebeaa9d9..881d0ef8 100644 --- a/j17/src/fr/hammons/slinc/modules/FSetModule17.scala +++ b/j17/src/fr/hammons/slinc/modules/FSetModule17.scala @@ -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 ) diff --git a/j19/src/fr/hammons/slinc/modules/FSetModule19.scala b/j19/src/fr/hammons/slinc/modules/FSetModule19.scala index 2dd89832..b4818e40 100644 --- a/j19/src/fr/hammons/slinc/modules/FSetModule19.scala +++ b/j19/src/fr/hammons/slinc/modules/FSetModule19.scala @@ -10,9 +10,6 @@ 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], @@ -20,22 +17,16 @@ given fsetModule19: FSetModule with ): 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 )