剑阁住房和城乡建设厅网站,做外贸的国外平台有哪些,微信群网站有哪些,店铺设计方案前言
在上篇文章中我们已经介绍了实现 Compose MultiPlatform 对 C/C 互操作的基本思路。
并且先介绍了在 kotlin native 平台使用 cinterop 实现与 C/C 的互操作。
今天这篇文章将补充在 jvm 平台使用 jni。
在 Compose MultiPlatform 中#xff0c;使用 jvm 平台的是 An…前言
在上篇文章中我们已经介绍了实现 Compose MultiPlatform 对 C/C 互操作的基本思路。
并且先介绍了在 kotlin native 平台使用 cinterop 实现与 C/C 的互操作。
今天这篇文章将补充在 jvm 平台使用 jni。
在 Compose MultiPlatform 中使用 jvm 平台的是 Android 端和 Desktop 端而安卓端可以直接使用安卓官方的 NDK 实现交叉编译但是 Desktop 不仅不支持交叉编译甚至连使用 Gradle 自动编译都没有。
所以本文重点主要在于实现 Desktop 的 jni 编译以及调用编译出来的二进制库。
Android 使用 jni
在介绍 Desktop 使用 jni 之前我们先回顾一下在 Android 中使用 jni并复用 Android 端的 C 代码给 Desktop 使用。
感谢谷歌的工作在安卓中使用 jni 非常简单我们只需要在 Android Studio 随便打开一个已有的项目然后依次选择菜单 File - New - New Module - Android Native Library保持默认参数点击 Finish 即可完成创建安卓端的 jni 模块。
这里我们以 jetBrains 的官方 Compose MultiPlatform 模板 项目作为示例 创建完成后需要注意Android studio 会自动修改项目 settings.gradle.kts 在其中添加一个插件 org.jetbrains.kotlin.android 这会导致编译错误 java.lang.IllegalArgumentException: Cannot provide multiple default versions for the same plugin.所以需要我们删掉新添加的这个插件 然后在 shared 模块中的 build.gradle.kts 文件的 Android 依赖部分引入 nativelib 模块
kotlin {// ……sourceSets {// ……val androidMain by getting {dependencies {// ……api(project(:nativelib))}}// ……}
}接着需要注意 nativelib 模块的两个文件 native.cpp 和 NativeLib.kt 我们看一下 nativelib 模块中的 nativelib.cpp 文件的默认内容
#include jni.h
#include stringextern C JNIEXPORT jstring JNICALL
Java_com_equationl_nativelib_NativeLib_stringFromJNI(JNIEnv* env,jobject /* this */) {std::string hello C;return env-NewStringUTF(hello.c_str());
}代码很简单就是返回一个字符串 “Hello from C”我们改成返回 “C”。
这里需要注意这个函数的名称 Java_com_equationl_nativelib_NativeLib_stringFromJNI
开头的 “Java” 是固定字符后面的 “com_equationl_nativelib_NativeLib” 表示从 java 调用时的类的包名类名最后的 “stringFromJNI” 才是这个函数的名称。
通过 jni 从 javakt中调用这个函数时必须确保其包名和类名与其一致才能成功调用。
然后查看 NativeLib.kt 文件
class NativeLib {external fun stringFromJNI(): Stringcompanion object {init {System.loadLibrary(nativelib)}}
}其中 external fun stringFromJNI(): String 表示需要调用的 c 函数名。
System.loadLibrary(nativelib) 表示加载 C 编译生成的二进制库这里我们无需关心具体的编译过程和编译产物只需要直接加载 nativelib 即可剩下的工作 NDK 已经替我们完成了。
最后我们来调用一下这个 C 函数。
不过在此之前先简单介绍一下我们用作示例的这个 Compose MultiPlatform 的内容它的 UI 就是一个按钮按钮默认显示 “Hello, World!”当点击按钮后会通过一个 expect 函数获取当前平台的名称然后显示到按钮上
OptIn(ExperimentalResourceApi::class)
Composable
fun App() {MaterialTheme {var greetingText by remember { mutableStateOf(Hello, World!) }var showImage by remember { mutableStateOf(false) }Column(Modifier.fillMaxWidth(), horizontalAlignment Alignment.CenterHorizontally) {Button(onClick {greetingText Hello, ${getPlatformName()}showImage !showImage}) {Text(greetingText)}AnimatedVisibility(showImage) {Image(painterResource(compose-multiplatform.xml),contentDescription Compose Multiplatform icon)}}}
}expect fun getPlatformName(): String所以接下来我们修改安卓平台的 getPlatformName 函数的 actual 实现由
actual fun getPlatformName(): String Android修改为
actual fun getPlatformName(): String NativeLib().stringFromJNI()这样它获取的名称就是来自 C 代码的 “C” 了。
运行代码可以看到完美符合预期 Desktop 使用 jni
上一节我们已经完成了在 Android 中使用 jni本节我们将在 Desktop 中也实现使用 jni并且复用上节中的 nativelib.cpp 文件。
因为直接使用 Gradle 编译 C 代码不是很方便而且还不支持交叉编译所以这里我们首先手动编译验证可行后再自己编写 gradle 脚本实现自动编译。
有关编写 gradle 脚本的基础知识可以阅读我之前的文章 Compose Desktop 使用中的几个问题分平台加载资源、编写Gradle 任务下载平台资源、桌面特有组件、鼠标键盘事件 了解。
首先我们可以使用命令 g nativelib.cpp -o nativelib.bin -shared -fPIC -I C:\Users\equationl\.jdks\corretto-19.0.2\include -I C:\Users\equationl\.jdks\corretto-19.0.2\include\win32 编译我们的 C 文件为当前平台可用的二进制文件。
上述命令中 nativelib.cpp 即需要编译的文件nativelib.bin 为输出的二进制文件C:\Users\equationl\.jdks\corretto-19.0.2\ 为你电脑上安装的任意的 jdk 目录。
输入 “ j d k P a t h / i n c l u d e 和 jdkPath/include 和 jdkPath/include和jdkPath/include/win32” 是因为这两个目录下有我们的 C 文件导入所需的头文件如 “jni.h” 。
切换到我们的 C 文件所在目录后执行上述命令编译 此时我们可以看到在 “./nativelib/src/main/cpp” 目录下已经生成了 nativelib.bin 文件。
注意在 macOS 上系统自带了 g 命令但是一般来说 Windows 系统没有自带 g 命令所以需要先自己安装 g
然后我们在 sahred 模块下的 desktopMain 包中新建一个文件 NativeLib.kt 注意该文件的包名需要和 C 定义的一致 然后编写该文件内容为
package com.equationl.nativelibclass NativeLib {external fun stringFromJNI(): Stringcompanion object {init {System.load(D:\\project\\ideaProject\\compose-multiplatform-c-test\\nativelib\\src\\main\\cpp\\nativelib.bin)}}
}可以看到在 Desktop 中加载二进制库和 Android 中略有不同它使用的是 System.load() 而不是 System.loadLibrary() 并且加载二进制文件时使用的是绝对路径。
这是因为我们无法在 Desktop 中像 Android 一样直接把二进制文件打包到指定的路径下并且直接使用库名通过 System.loadLibrary() 加载所以只能使用绝对路径加载外部二进制文件。
这里我们把加载的文件路径写为了先前生成的 nativelib.bin 的路径。
接着依旧是修改 dektop 的 getPlatformName 函数的实现为
actual fun getPlatformName(): String NativeLib().stringFromJNI()然后运行 Desktop 程序 运行结果完美符合预期。
为 Desktop 实现自动编译 C
在上一节中我们已经实现了 Desktop 使用 jni 并验证了可行性但是目前还是手动编译代码这显然是不现实的所以我们本节将讲解如何自己编写脚本实现自动编译。
另外上一节中我们说过 Dektop 加载二进制文件使用的是绝对路径所以我们需要将编译生成的二进制文件放到指定位置并打包进 Desktop 程序安装包中Desktop 在安装时会自动将这个文件解压到指定路径关于这个的基础知识还是可以看我的文章 Compose Desktop 使用中的几个问题分平台加载资源、编写Gradle 任务下载平台资源、桌面特有组件、鼠标键盘事件 了解。
首先需要指定一下资源文件目录在 desktopApp 模块的 buiuld.gradle.kts 文件中添加以下内容
compose.desktop {application {// ……nativeDistributions {// ……appResourcesRootDir.set(project.layout.projectDirectory.dir(resources))}}
}指定资源目录为 resources 。
然后依旧是在这个文件中添加一个函数 runCommand用于执行 shell 命令
fun runCommand(command: String, timeout: Long 120): PairBoolean, String {val process ProcessBuilder().command(command.split( )).directory(rootProject.projectDir).redirectOutput(ProcessBuilder.Redirect.INHERIT).redirectError(ProcessBuilder.Redirect.INHERIT).start()process.waitFor(timeout, TimeUnit.SECONDS)val result process.inputStream.bufferedReader().readText()val error process.errorStream.bufferedReader().readText()return if (error.isBlank()) {Pair(true, result)}else {Pair(false, error)}
}代码很简单接收一个字符串表示的 shell 命令返回一个 Pair 第一个 booean 数据表示是否执行成功第二个 String 是输出内容。
接着注册一个 task
tasks.register(compileJni) { }修改原有的 prepareAppResources task添加上我们刚注册的 compileJni 为它的依赖
gradle.projectsEvaluated {tasks.named(prepareAppResources) {dependsOn(compileJni)}
}这里的修改依赖需要加在 gradle.projectsEvaluated 语句中因为 prepareAppResources 这个 task 推迟了注册如果不在项目配置完成后再修改依赖的话会报 prepareAppResources 不存在。
注这里的 prepareAppResources 是 task 模块中用于执行复制和打包资源文件的 task所以我们把自定义的 compileJni 添加成它的依赖以保证在它之前执行。
另外这里必须明确保证 compileJni 在 prepareAppResources 之前执行否则由于我们的 compileJni 任务的输出路径和 prepareAppResources 任务的输出路径冲突会导致编译失败具体后面详细解释。
接着在 compileJni task 中编写我们的编译逻辑我们先看一下完整的代码然后再逐一解释
tasks.register(compileJni) {description compile jni binary file for desktopval resourcePath File(rootProject.projectDir, desktopApp/resources/common/lib/)val binFilePath File(resourcePath, nativelib.bin)val cppFileDirectory File(rootProject.projectDir, nativelib/src/main/cpp)val cppFilePath File(cppFileDirectory, nativelib.cpp)// 指定输入、输出文件用于增量编译inputs.dir(cppFileDirectory)outputs.file(binFilePath)doLast {project.logger.info(compile jni for desktop running……)val jdkFile org.gradle.internal.jvm.Jvm.current().javaHomeval systemPrefix: Stringval os: OperatingSystem DefaultNativePlatform.getCurrentOperatingSystem()if (os.isWindows) {systemPrefix win32}else if (os.isMacOsX) {systemPrefix darwin}else if (os.isLinux) {systemPrefix linux}else {project.logger.error(UnSupport System for compiler cpp, please compiler manual)returndoLast}val includePath1 jdkFile.resolve(include)val includePath2 includePath1.resolve(systemPrefix)if (!includePath1.exists() || !includePath2.exists()) {val msg ERROR: $includePath2 not found!\nMaybe its because you are using JetBrain Runtime (Jbr)\nTry change Gradle JDK to another jdk which provide jni supportthrow GradleException(msg)}project.logger.info(Check Desktop Resources Path……)if (!resourcePath.exists()) {project.logger.info(${resourcePath.absolutePath} not exists, create……)mkdir(resourcePath)}val runTestResult runCommand(g --version)if (!runTestResult.first) {throw GradleException(Error: Not find command g, Please install it and add to your system environment path\n${runTestResult.second})}val command g ${cppFilePath.absolutePath} -o ${binFilePath.absolutePath} -shared -fPIC -I ${includePath1.absolutePath} -I ${includePath2.absolutePath}project.logger.info(running command $command……)val compilerResult runCommand(command)if (!compilerResult.first) {throw GradleException(Command run fail: ${compilerResult.second})}project.logger.info(compilerResult.second)project.logger.lifecycle(compile jni for desktop all done)}
}首先在 task 顶级定义了四个路径 resourcePath 、 binFilePath 、cppFileDirectory 和 cppFilePath分别表示需要存放二进制文件的资源目录、二进制文件输出路径、C文件存放目录和需要编译的具体 C 文件路径。
rootProject.projectDir 返回的是当前项目的根目录。
接着我们通过 inputs.dir() 方法添加了该 task 的输入路径。
outputs.file 方法添加了该 task 的输出文件。
定义输入路径和输出文件与我们这里需要执行的编译没有直接关联这里定义这个两个路径是为了让 Gradle 实现增量编译即只有在上次编译完成后输入路径的中的文件内容发生了变化或输出文件发生了变化才会继续执行这个 task否则会认为这个 task 没有变化不会执行表现在编译输出日志则为 Task :desktopApp:compileJni UP-TO-DATE
接下来我们的代码写在了 doLast { } 语句中则表示里面的代码只有在编译阶段才会执行在配置阶段不会执行。
在其中的 org.gradle.internal.jvm.Jvm.current().javaHome 返回的是当前项目 Gradle 使用的 jdk 根目录。
然后我们需要拼接出编译时需要导入的两个 jdk 路径 includePath1 和 includePath2 其中的 includePath2 不同的系统名称不一样所以需要判断一下当前编译使用的系统并更改该值。 可以通过 DefaultNativePlatform.getCurrentOperatingSystem().isXXX 判断当前是否是某个系统。
接着检查存放二进制文件的目录是否存在不存在则创建。
下一步是使用 g --version 测试是否安装了 g 。
最后拼接出编译命令后执行编译
g ${cppFilePath.absolutePath} -o ${binFilePath.absolutePath} -shared -fPIC -I ${includePath1.absolutePath} -I ${includePath2.absolutePath}
此时如果编译成功那么二进制文件会输出到我们指定的 dektop 资源目录下。
我们现在只需要修改 dektop 加载二进制文件的代码为
val libFile File(System.getProperty(compose.application.resources.dir)).resolve(lib).resolve(nativelib.bin)
System.load(libFile.absolutePath)上述代码中 System.getProperty(compose.application.resources.dir) 返回的是我们最开始在 Gradle 中定义的资源打包安装解压后在系统上的绝对路径。
至此我们的自动编译已经完成
最后来说一下我们前面提到的为什么我们的 compileJni task 必须在 prepareAppResources 之前执行我们现在直接把原本的修改 prepareAppResources 依赖于 compileJni 改成 Desktop 模块执行的第一个 task compileKotlinJvm 依赖 compileJni
tasks.named(compileKotlinJvm) {dependsOn(compileJni)
}运行后会看到报错
A problem was found with the configuration of task :desktopApp:prepareAppResources (type Sync).- Gradle detected a problem with the following location: /Users/equationl/AndroidStudioProjects/life-game-compose/desktopApp/resources/common.Reason: Task :desktopApp:prepareAppResources uses this output of task :desktopApp:compileJni without declaring an explicit or implicit dependency. This can lead to incorrect results being produced, depending on what order the tasks are executed.Possible solutions:1. Declare task :desktopApp:compileJni as an input of :desktopApp:prepareAppResources.2. Declare an explicit dependency on :desktopApp:compileJni from :desktopApp:prepareAppResources using Task#dependsOn.3. Declare an explicit dependency on :desktopApp:compileJni from :desktopApp:prepareAppResources using Task#mustRunAfter.简单说就是 prepareAppResources 和 compileJni 都声明了同一个输出路径除非明确指定它们两个之间的依赖关系否则编译会出现问题。
其实也很好理解他们的输出路径都是一个如果不明确依赖关系的话增量编译就永远不会触发了永远都将是全量编译。
而在这里我们的需求是首先使用 compileJni 生成二进制文件后由 prepareAppResources 将其打包所以自然应该是写成 prepareAppResources 依赖于 compileJni 。
最后还是需要强调一点Desktop 编译 C 是不支持交叉编译的也就是说在 Windows 只能编译 Windows 的程序在 macOS 只能 编译 macOS 的程序。
其实即使 C 可以交叉编译也没用因为 Compose Desktop 并不支持交叉编译哈哈哈。
参考资料
Native dependency in Kotlin/Multiplatform — part 2: JNI for JVM AndroidKotlin JNI for Native Code