Skip to content

Commit

Permalink
Migrate native code to use Java FFM API
Browse files Browse the repository at this point in the history
  • Loading branch information
DRSchlaubi committed Aug 17, 2023
1 parent 78434fa commit 35a7f5b
Show file tree
Hide file tree
Showing 18 changed files with 217 additions and 332 deletions.
14 changes: 11 additions & 3 deletions .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion app/desktop/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import org.jetbrains.kotlin.org.jline.utils.OSUtils

plugins {
kotlin("jvm")
kotlin("plugin.serialization")
id("org.jetbrains.compose")
}

Expand All @@ -18,6 +19,7 @@ compose.desktop {
application {
mainClass = "dev.schlaubi.tonbrett.app.desktop.MainKt"

jvmArgs("--enable-native-access=ALL-UNNAMED", "--enable-preview")
nativeDistributions {
modules(
"java.naming" // required by logback
Expand Down Expand Up @@ -93,7 +95,7 @@ tasks {
}
}
from(file("uwp_helper/target/release")) {
include("*.exe")
include("*.dll")
}
from(file("msix"))
into(buildDir.resolve("msix-workspace"))
Expand All @@ -115,6 +117,10 @@ tasks {
delete(buildDir.resolve("msix-workspace").resolve("update_msix_version.ps1"))
}

withType<JavaCompile> {
options.compilerArgs.add("--enable-preview")
}

afterEvaluate {
"packageReleaseDistributionForCurrentOS" {
if (OSUtils.IS_WINDOWS) {
Expand Down
1 change: 1 addition & 0 deletions app/desktop/rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
-dontwarn ch.qos.logback.**
-dontwarn okhttp3.**
-dontwarn io.ktor.**
-dontwarn dev.schlaubi.tonbrett.app.desktop.NativeUtil

# kmongo
-keep class org.litote.kmongo.id.UUIDStringIdGeneratorProvider
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package dev.schlaubi.tonbrett.app.desktop;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;
import java.net.URI;

/**
* Utility class for calling {@code uwp_helper} functions.
* <p>
* Implemented in Java because {@code @PolymorphicSignature} doesn't seem to work in Kotlin
*/
public class NativeUtil {

private static final NativeUtil instance = new NativeUtil();
private final Linker linker = Linker.nativeLinker();
private final SymbolLookup uwpHelper = SymbolLookup.libraryLookup("uwp_helper", SegmentScope.global());

private final MethodHandle launchUriMethod = linker.downcallHandle(
uwpHelper.find("launch_uri").orElseThrow(),
FunctionDescriptor.ofVoid(ValueLayout.ADDRESS)
);

private final MethodHandle getAppdataFolder = linker.downcallHandle(
uwpHelper.find("get_appdata_folder").orElseThrow(),
FunctionDescriptor.ofVoid(ValueLayout.ADDRESS)
);

private final MethodHandle getAppdataFolderPathLength = linker.downcallHandle(
uwpHelper.find("get_appdata_folder_path_length").orElseThrow(),
FunctionDescriptor.of(ValueLayout.JAVA_LONG)
);

/**
* Tries to launch the URI using the UWP {@code Launcher}.
*
* @param uri the {@link URI} to launch
* @throws Throwable if an error occurs
*/
public static void launchUri(@NotNull URI uri) throws Throwable {
try (var arena = Arena.openConfined()) {
var url = arena.allocateUtf8String(uri.toString());

instance.launchUriMethod.invoke(url);
}
}

private static long getAppdataFolderLength() throws Throwable{
return (long) instance.getAppdataFolderPathLength.invoke();
}

/**
* Tries to retrieve the current UWP app data folder.
*
* @return the absolute path to the folder
* @throws Throwable if an error occurrs
*/
@Nullable
public static String getAppdataFolder() throws Throwable {
var length = getAppdataFolderLength();
if (length == 0) {
return null;
}
try(var arena = Arena.openConfined()) {
var buffer = arena.allocateArray(ValueLayout.JAVA_CHAR, length);
instance.getAppdataFolder.invoke(buffer);
return new String(buffer.toArray(ValueLayout.JAVA_CHAR));
}
}
}
2 changes: 0 additions & 2 deletions app/desktop/src/main/kotlin/AuthorizationServer.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package dev.schlaubi.tonbrett.app.desktop

import dev.schlaubi.tonbrett.app.api.Config
import dev.schlaubi.tonbrett.app.api.getUrl
import dev.schlaubi.tonbrett.app.api.saveConfig
import dev.schlaubi.tonbrett.common.authServerPort
import io.ktor.http.*
import io.ktor.server.application.*
Expand Down
9 changes: 9 additions & 0 deletions app/desktop/src/main/kotlin/ConfigBasedAppContext.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package dev.schlaubi.tonbrett.app.desktop

import dev.schlaubi.tonbrett.app.api.AppContext

abstract class ConfigBasedAppContext : AppContext() {
override var token: String
get() = getConfig().sessionToken ?: error("Please sign in")
set(value) = saveConfig(Config(sessionToken = value))
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
@file:OptIn(ExperimentalSerializationApi::class)

package dev.schlaubi.tonbrett.app.api
package dev.schlaubi.tonbrett.app.desktop

import kotlinx.serialization.EncodeDefault
import kotlinx.serialization.ExperimentalSerializationApi
Expand All @@ -15,21 +15,16 @@ import kotlin.io.path.*

private val LOG = KotlinLogging.logger { }

val windowsAppDataFolder by lazy {
runCatching {
val process = ProcessBuilder()
.command("get_appdata_folder.exe")
.redirectError(ProcessBuilder.Redirect.INHERIT)
.start()
process.waitFor()
val windowsAppDataFolder: String? by lazy {
val result = runCatching {
NativeUtil.getAppdataFolder()
}

// If opening this in Android Studio this is a false positive, since Android doesn't
// actually call this
@Suppress("NewApi")
process.inputStream.bufferedReader().readText().also {
LOG.debug { "AppData determined to be $it" }
}
}.getOrNull()
if (result.isFailure) {
result.exceptionOrNull()!!.printStackTrace()
}

result.getOrNull()
}

@Serializable
Expand Down
5 changes: 3 additions & 2 deletions app/desktop/src/main/kotlin/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ import cafe.adriel.lyricist.LocalStrings
import dev.schlaubi.tonbrett.app.ColorScheme
import dev.schlaubi.tonbrett.app.ProvideImageLoader
import dev.schlaubi.tonbrett.app.TonbrettApp
import dev.schlaubi.tonbrett.app.api.*
import dev.schlaubi.tonbrett.app.api.ProvideContext
import dev.schlaubi.tonbrett.app.api.getUrl
import dev.schlaubi.tonbrett.app.title
import dev.schlaubi.tonbrett.client.href
import dev.schlaubi.tonbrett.common.Route
Expand Down Expand Up @@ -116,7 +117,7 @@ private fun ApplicationScope.startActualApplication(
CompositionLocalProvider(LocalWindowExceptionHandlerFactory provides exceptionHandler) {
TonbrettWindow {
val context = remember {
object : AppContext() {
object : ConfigBasedAppContext() {
override fun reAuthorize() {
window.isMinimized = true
main(reAuthorize = true, uwp = uwp) {
Expand Down
10 changes: 2 additions & 8 deletions app/desktop/src/main/kotlin/URIUtil.kt
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
package dev.schlaubi.tonbrett.app.desktop

import dev.schlaubi.tonbrett.app.api.windowsAppDataFolder
import java.awt.Desktop
import java.net.URI

fun browseUrl(url: URI) {
// Desktop#browse() internally uses ShellExecute, which doesn't work in UWP apps, therefore,
// We use a Rust binary, which calls:
// https://learn.microsoft.com/de-de/uwp/api/windows.system.launcher.launchuriasync?view=winrt-22621
if (System.getProperty("os.name").contains("windows", ignoreCase = true) && windowsAppDataFolder != null) {
check(Runtime.getRuntime().exec(arrayOf("url_launcher.exe", "-u", url.toString())).waitFor() == 0) {
"url_launcher.exe failed"
}
if (System.getProperty("os.name").contains("windows", ignoreCase = true)) {
NativeUtil.launchUri(url)
} else {
val desktop = Desktop.getDesktop()
if (desktop.isSupported(Desktop.Action.BROWSE)) {
Expand Down
Loading

0 comments on commit 35a7f5b

Please sign in to comment.