Skip to content

Commit

Permalink
clean up ffc impl, added initial tests
Browse files Browse the repository at this point in the history
  • Loading branch information
AlmasB committed Dec 26, 2024
1 parent 0315f0d commit c5d9bf5
Show file tree
Hide file tree
Showing 5 changed files with 180 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/
package com.almasb.fxgl.core.reflect;

import com.almasb.fxgl.core.util.EmptyRunnable;
import com.almasb.fxgl.logging.Logger;

import java.lang.foreign.*;
Expand Down Expand Up @@ -36,8 +37,6 @@ public final class ForeignFunctionCaller {
private static final AtomicInteger threadCount = new AtomicInteger(0);

private List<Path> libraries;
private Arena arena;
private Linker linker;
private List<SymbolLookup> lookups = new ArrayList<>();
private Map<String, MemorySegment> functionsAddresses = new HashMap<>();
private Map<String, MethodHandle> functions = new HashMap<>();
Expand All @@ -46,7 +45,10 @@ public final class ForeignFunctionCaller {
public BlockingQueue<Consumer<ForeignFunctionContext>> executionQueue = new ArrayBlockingQueue<>(1000);
private AtomicBoolean isRunning = new AtomicBoolean(true);

private FFCThread thread;
private Thread thread;

private Runnable onLoaded = EmptyRunnable.INSTANCE;
private Runnable onUnloaded = EmptyRunnable.INSTANCE;
private boolean isLoaded = false;

/**
Expand All @@ -58,6 +60,14 @@ public ForeignFunctionCaller(List<Path> libraries) {
this.libraries = new ArrayList<>(libraries);
}

public void setOnLoaded(Runnable onLoaded) {
this.onLoaded = onLoaded;
}

public void setOnUnloaded(Runnable onUnloaded) {
this.onUnloaded = onUnloaded;
}

public void load() {
if (isLoaded) {
log.warning("Already loaded: " + libraries);
Expand All @@ -66,7 +76,8 @@ public void load() {

isLoaded = true;

thread = new FFCThread(this::threadTask);
thread = new Thread(this::threadTask, "FFCThread-" + threadCount.getAndIncrement());
thread.setDaemon(true);
thread.start();

// TODO: wait until libs are loaded and loop entered
Expand All @@ -76,18 +87,16 @@ public void load() {
private void threadTask() {
log.debug("Starting native setup task");

try (var a = Arena.ofConfined()) {
arena = a;
linker = Linker.nativeLinker();

try (var arena = Arena.ofConfined()) {
libraries.forEach(file -> {
var lookup = SymbolLookup.libraryLookup(file, arena);
lookups.add(lookup);
});

context = new ForeignFunctionContext(arena, linker, lookups);
context = new ForeignFunctionContext(arena, Linker.nativeLinker(), lookups);

log.debug("Native libs loaded and context created");
onLoaded.run();

while (isRunning.get()) {
try {
Expand All @@ -101,6 +110,11 @@ private void threadTask() {
} catch (Throwable e) {
log.warning("FFCThread task failed", e);
}

// the libraries loaded iva SymbolLookup are unloaded
// when the associated arena is closed, i.e. when above try-catch completes
// however, in practice, it appears there is a delay before the lib is fully closed
onUnloaded.run();
}

private MethodHandle getFunctionImpl(String name, FunctionDescriptor fd) {
Expand All @@ -125,7 +139,7 @@ private MethodHandle getFunctionImpl(String name, FunctionDescriptor fd) {
functionsAddresses.put(name, functionAddress);
}

MethodHandle function = linker.downcallHandle(functionAddress, fd);
MethodHandle function = context.linker.downcallHandle(functionAddress, fd);

functions.put(functionID, function);

Expand Down Expand Up @@ -160,20 +174,21 @@ public void execute(Consumer<ForeignFunctionContext> functionCall) {
}

public void unload() {
// TODO: isLoaded = false?
// TODO: if not loaded ignore, same for overload below
isRunning.set(false);

// TODO: execute poison pill to shutdown thread
unload(_ -> {});
}

/**
* @param libExitFunctionCall the last function to call in the loaded library(-ies)
* @param libExitFunctionCall the last function to call in the loaded library(-ies),
* do not schedule any other execute() operations within the call
*/
public void unload(Consumer<ForeignFunctionContext> libExitFunctionCall) {
isRunning.set(false);
// TODO: isLoaded = false?
// TODO: if not loaded ignore?

execute(libExitFunctionCall);
execute(context -> {
libExitFunctionCall.accept(context);
isRunning.set(false);
});
}

public final class ForeignFunctionContext {
Expand Down Expand Up @@ -220,10 +235,4 @@ public MemorySegment allocateCharArrayFrom(String s) {
return arena.allocateFrom(s);
}
}

private static class FFCThread extends Thread {
FFCThread(Runnable task) {
super(task, "FFCThread-" + threadCount.getAndIncrement());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@

package com.almasb.fxgl.core.util

import com.almasb.fxgl.core.util.Platform.*
import com.almasb.fxgl.logging.Logger
import java.net.URL
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.StandardCopyOption

Expand All @@ -23,6 +25,12 @@ class ResourceExtractor {

private val log = Logger.get(ResourceExtractor::class.java)

private val nativeLibResourceDirNames = mapOf(
WINDOWS to "windows64",
LINUX to "linux64",
MAC to "mac64"
)

/**
* Extracts the file at jar [url] as a [relativeFilePath].
* Note: the destination file will be overwritten.
Expand Down Expand Up @@ -52,5 +60,34 @@ class ResourceExtractor {

return file.toUri().toURL()
}

/**
* Extracts the file at jar nativeLibs/platformDirName/[libName].
* Note: the destination file will be overwritten.
*
* @return the path on the local file system to the extracted file
*/
@JvmStatic fun extractNativeLibAsPath(libName: String): Path {
return Paths.get(extractNativeLib(libName).toURI())
}

/**
* Extracts the file at jar nativeLibs/platformDirName/[libName].
* Note: the destination file will be overwritten.
*
* @return the url on the local file system of the extracted file
*/
@JvmStatic fun extractNativeLib(libName: String): URL {
val platform = Platform.get()

if (nativeLibResourceDirNames.containsKey(platform)) {
val dirName = nativeLibResourceDirNames[platform]

return extract(javaClass.getResource("/nativeLibs/$dirName/$libName"), libName)

} else {
throw RuntimeException("FXGL does not have libraries for this platform: $platform")
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* FXGL - JavaFX Game Library. The MIT License (MIT).
* Copyright (c) AlmasB ([email protected]).
* See LICENSE for details.
*/

package com.almasb.fxgl.core.reflect

import com.almasb.fxgl.core.util.ResourceExtractor
import org.hamcrest.CoreMatchers
import org.hamcrest.CoreMatchers.*
import org.hamcrest.MatcherAssert
import org.hamcrest.MatcherAssert.*
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable
import org.junit.jupiter.api.condition.EnabledOnOs
import org.junit.jupiter.api.condition.OS
import java.lang.foreign.FunctionDescriptor
import java.lang.foreign.ValueLayout
import java.nio.file.Files
import java.util.concurrent.CountDownLatch

/**
* @author Almas Baim (https://github.com/AlmasB)
*/
class ForeignFunctionCallerTest {

@EnabledOnOs(OS.WINDOWS)
@Test
@EnabledIfEnvironmentVariable(named = "CI", matches = "true")
fun `Downcall a native function`() {
val file = ResourceExtractor.extractNativeLibAsPath("native-lib-test.dll")

val countDown = CountDownLatch(1)

val ffc = ForeignFunctionCaller(listOf(file))
ffc.setOnLoaded {
countDown.countDown()
}

ffc.load()

ffc.execute {
val result = it.call(
"testDownCall",
FunctionDescriptor.of(
ValueLayout.JAVA_INT,
ValueLayout.JAVA_INT
),
5
) as Int

assertThat(result, `is`(25))
}

ffc.unload()

countDown.await()

// block some time until the native lib is fully unloaded
Thread.sleep(2500)

Files.deleteIfExists(file)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* FXGL - JavaFX Game Library. The MIT License (MIT).
* Copyright (c) AlmasB ([email protected]).
* See LICENSE for details.
*/

package com.almasb.fxgl.core.util

import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.`is`
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable
import java.nio.file.Files
import java.nio.file.Paths

/**
* @author Almas Baim (https://github.com/AlmasB)
*/
class ResourceExtractorTest {

@Test
@EnabledIfEnvironmentVariable(named = "CI", matches = "true")
fun `File is correctly extracted from resources`() {
val testFile = Paths.get(System.getProperty("user.home"))
.resolve(".openjfx")
.resolve("cache")
.resolve("fxgl-21")
.resolve("test_file.txt")

Files.deleteIfExists(testFile)

assertTrue(Files.notExists(testFile))

val file = Paths.get(
ResourceExtractor.extract(javaClass.getResource("/com/almasb/fxgl/localization/LocalEnglish.properties"), "test_file.txt").toURI()
)

val s = Files.readString(file)

Files.deleteIfExists(file)

assertThat(s, `is`("data.key = Data2"))
}
}
Binary file not shown.

0 comments on commit c5d9bf5

Please sign in to comment.