I'm attempting to migrate an Android-only compose ...
# compose-desktop
r
I'm attempting to migrate an Android-only compose codebase to Android+Compose Desktop. The Android project uses
android { externalNativeBuild { ... } }
to build a C shared library using the Android NDK toolchain + cmake. Does anyone have any advice for how to approach this for compose desktop, since
externalNativeBuild
is part of the android gradle plugin, and therefore can't be used for compose desktop?
d
To use C library on JVM you can use JNI
r
Hi Dima -- I'm doing exactly that. I have JNI bindings for Android, but I also need to use those JNI bindings on desktop. This doesn't work, because compilation of the JNI code happens as part of the android plugin.
j
You have to compile it separately and bundle it in the jar. Then, at runtime, copy it out to a temp folder and load it by path.
๐Ÿ™Œ 1
h
Here's how I automatically compile a jni library in gradle. It's useful for development, but doesn't allow binaries to be distributed.
Copy code
compose.desktop {
    application {
        mainClass = "MainKt"
        jvmArgs("-Djava.library.path=libs/jni")
...
    }
}
Copy code
tasks.register("compileCPP") {
    exec {
        workingDir("native")
        inputs.dir(workingDir)
        val libName = System.mapLibraryName("native")
        outputs.file("$projectDir/libs/jni/$libName")
        val arch = System.getProperty("os.arch")
        val os = System.getProperty("os.name")
        val buildPath = "${layout.buildDirectory.get()}/cmake/$arch/$os"
        commandLine("cmake", "-DCMAKE_BUILD_TYPE=Release", "CMakeLists.txt", "-B", buildPath)
        doLast {
            exec {
                commandLine("cmake", "--build", buildPath)
            }
            copy {
                from("$buildPath/$libName")
                into("$projectDir/libs/jni")
            }
        }
        outputs.upToDateWhen { true }
    }
}
tasks.getByPath("desktopProcessResources").dependsOn("compileCPP")
๐Ÿ™Œ 1
d
h
I've never managed to apply this tutorial to a KMP project.
j
If you want a complex, real-world example, Zipline does this. We have a KMP library that bundles a C library which works on Android via AGP's built-in CMake support, JVM via manual CMake invocation, and native using cinterop. https://github.com/cashapp/zipline/blob/trunk/zipline/build.gradle.kts
h
I've looked at this project before, I see they use the "com.android.library" plugin for android and "co.touchlab.cklib" for Kotlin Native but I'm not sure if the JNI library is compiled for anything other than Android. In any case, I can't find any clues.
j
We support JVM on Linux and Mac
h
ha ok, so the process isn't integrated into Gradle, which was my initial problem.
j
I mean, it could be if you wanted it to be.
Since we're not (yet) using Zig to cross-compile for desktop we build with CMake on Linux and MacOS separately (for x64 and ARM), and then copy those native libraries into the jar resources in the final JVM build.
If you integrated it into Gradle you would need to use Zig to cross-compile to all supported targets, or if you stick with regular clang or whatever you'd need the sysroots of all those targets.
The Android NDK is nice enough to provide sysroots for its targets so we can cross-compile to all its architectures on a single machine. With the JVM you have to do a lot more yourself. Or use Zig. Everyone should just use Zig.
h
I didn't know Zig, I'll look into it, thanks for the tip!
h
OMG, it's amazing, I need to play with it
r
https://github.com/cashapp/zipline/blob/trunk/zipline/build.gradle.kts
This looks like a treasure trove, but I don't see the manual cmake invocation for jvm?
r
... and I was planning on using zig, actually ๐Ÿ™‚. Go is my primary language, and Go's trivial cross-compiling is something I sorely miss. Excited that Zig is bringing this elsewhere.
Ahh, I was looking to see this kicked off by gradle. No wonder it wasn't there
(thank you)
j
Yeah we always talked about having Gradle build the host machine's version for simplicity, but the native code changes so infrequently that we tell people to just run
.github/workflows/build_mac.sh
once and basically be done with it for local dev
Not great, certainly, but it works well enough because so few people contribute
r
Ah yeah. In my case, the native code changes very frequently (it's the core app logic), so needs to be part of the build process. I've been shocked at how difficult this is to do in a way that will work for multiplatform (jvm desktop, android). At this point I think I'm going to do as little as possible in gradle because it's been an absolute nightmare. I wonder how on earth anyone becomes productive in this ecosystem?
h
so I tested zig today and I'm quite satisfied. I used cmake with a toolchain file this way:
Copy code
cmake -G Ninja -DCMAKE_TOOLCHAIN_FILE=toolchain-Linux-aarch64.cmake -DCMAKE_BUILD_TYPE=Release CMakeLists.txt -B './build/Linux/aarch64'
cmake --build './build/Linux/aarch64'
toolchain-Linux-aarch64.cmake
Copy code
set(CMAKE_SYSTEM_NAME "Linux")
set(CMAKE_SYSTEM_PROCESSOR "aarch64")
set(CMAKE_C_COMPILER "zig" cc -target aarch64-linux-gnu)
set(CMAKE_CXX_COMPILER "zig" c++ -target aarch64-linux-gnu)
I'm on Linux and I can compile for different architectures for Linux and Darwin. Unfortunately, to cross compile for Windows I encounter many errors with mingw. As an alternative, I use "mingw64-cmake" instead of cmake and it works very well.
r
@Henri Gourvest did exactly the same, on a windows host machine. Got cross compiling for [x86_64, aarch64] + [windows, linux] working.
Cross compiling the jni bindings is slightly complicated by the fact that there are different jni headers per platform. And I don't really want to have to download N different JDKs to build. After looking at the headers across JDKs, jni.h is identical, and jni_md.h only has 2 variants: "unix" and "windows". There are more differences if you're trying to use AWT, but I'm not. To avoid downloading multiple JDKs, I manually made a jni include dir, and configured the toolchain to point there for JAVA_INCLUDE_PATH and JAVA_INCLUDE_PATH2, e.g. toolchains/linux-x86_64.cmake
Copy code
set(JAVA_INCLUDE_PATH "${CMAKE_CURRENT_LIST_DIR}/../include")
set(JAVA_INCLUDE_PATH2 "${JAVA_INCLUDE_PATH}/unix")
and then in my CMakeLists.txt
Copy code
set(JAVA_AWT_LIBRARY NotNeeded)
set(JAVA_AWT_INCLUDE_PATH NotNeeded)
set(JAVA_JVM_LIBRARY NotNeeded)
find_package(JNI REQUIRED)
which then gives me a JNI::JNI target I can use with target_link_libraries I haven't tested Android yet, but it should "just work" to, because find_package(JNI) is meant to be supported by the ndk toolchain too.
Copying the jni headers from the jdk would be a bad idea if they ever changed, but the last change I could see was 7 years ago, and that might have been the initial commit ๐Ÿ™‚
j
Plus even if they did change they won't change incompatibly or it would break all existing JNI code
๐Ÿ’ฏ 1
r
Next step for me is to use zig as the cross-compiler for the cgo portions of my golang library, where the meat of my application actually is. Can't think why that wouldn't work though. And the non-cgo portions of golang will cross compile without any toolchain (which totally ruins working in other languages ๐Ÿ˜‰)
h
jni_md.h which contains the JNIEXPORT macro is very different between windows and linux, when I compile under linux the JNI methods are not exported from the dll. I'll have to find a way to provide a specific include.
r
You need to keep both the windows and linux versions around, and use the right one. A picture might explain this better ๐Ÿ™‚
h
I did something similar, thank you.
๐Ÿ’ฏ 1