编程技术记录

世界你好!

前言

本文成文环境:

  • macOS 26
  • Xcode 26 以及 iOS SDK
  • Android Studio MeerKat Feature Drop 以及 Android SDK、NDK
  • JDK 17
  • Gradle 8.11.1

Kotlin Multiplatfrom 通过Cinterop工具调用C/C++方法,也可以通过JNI规范调用C/C++方法。

本质上,都是调用C方法,C++方法需要通过extern "C"修饰为C方法。

本文将介绍Kotlin/Native在Android和iOS环境,如何调用C/C++方法。

Android Studio

推荐使用 Android Studio 开发Kotlin/Native (Kotlin更像是一款Android开发语言)

Gradle

Android Studio的御用构建工具(相比其构建工具,复杂且难用)

CMake

一个事实上的C/C++构建标准工具,在Android和iOS环境调用各自平台的工具链构建C/C++。

创建 Kotlin Multiplatfrom 工程

如果Android Studio没有 Kotlin Multiplatfrom 工程模版,则需要先安装 Kotlin Multiplatfrom插件。
安装完成后,使用 Kotlin Multiplatfrom 工程模版创建一个工程(创建过程中,需要选择iOS的包管理方式,选择cocoapods)

镜像

因为环大陆的网络墙问题,可能需要配置下镜像仓库

1、找到工程根目录下的settings.gradle.kts,添加下镜像仓库(下面配置的是阿里云的maven仓库)

// settings.gradle.kts

pluginManagement {
    repositories {
        maven{ setUrl("https://maven.aliyun.com/repository/public") }
        maven{ setUrl("https://maven.aliyun.com/repository/central") }
        maven{ setUrl("https://maven.aliyun.com/repository/gradle-plugin") }
...
...
...
    }
}

dependencyResolutionManagement {
    repositories {
        maven{ setUrl("https://maven.aliyun.com/repository/public") }
        maven{ setUrl("https://maven.aliyun.com/repository/central") }
        maven{ setUrl("https://maven.aliyun.com/repository/gradle-plugin") }
...
...
...
    }
}

2、找到根目录下gradle/wrapper/gradle-wrapper.properties,替换为distributionUrl

#gradle/wrapper/gradle-wrapper.properties

#distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.11.1-all.zip

bin是二进制工具,all包含二进制和源码,gradle工具构建时可能会下载源码。

修改shared模块

默认情况下,创建的工程会有一个shared模块
展开后,可以看到src目录下有androidMain、commonMain、iosMain

使用Android Studio在shared模块中添加C/C++(鼠标右键选中shared模块,Add C/C++ to Module),这样就添加了Android Native相关代码,目录如下:

shared
        |_src
                |_commonMain (通用方法)
                    |_kotlin
                        |_com.example.kmpapptest(包名)
                            |_Platform.kt
                |_androidMain (android平台相关的方法)
                    |_kotlin
                        |_com.example.kmpapptest(包名)
                            |_Platform.android.kt
                |_iosMain (ios平台相关的方法)
                    |_kotlin
                        |_com.example.kmpapptest(包名)
                            |_Platform.ios.kt
                |_main (C/C++方法)
                    |_cpp
                        |_CMakeLists.txt (Android so构建配置)
                        |_main.cpp

因为我们使用的Android Studio的Add C/C++ to Module菜单创建的C/C++目录,所以在shared模块的build.gradle.kts里会自动添加Android平台的JNI构建任务

android {
    ...
    defaultConfig {
        ...
        externalNativeBuild {
            cmake {
                cppFlags += ""
            }
        }
        ...
    }
    ...
    externalNativeBuild {
        cmake {
            path = file("src/main/cpp/CMakeLists.txt")
            version = "3.22.1"
        }
    }
    ...
}

expect/actual

kotlin关键字 expect用来声明方法,actul标记实现方法。

我们在commonMain 中的Platform.kt声明(expect)一个 get_native_message()方法

// shared/src/commonMain/kotlin/com/example/kmpapptest/Platform.kt

package com.example.kmpapptest

expect fun get_native_message(): string 

在androidMain和iOSmain各自实现(actual)这个get_native_message方法

// shared/src/androidMain/kotlin/com/example/kmpapptest/Platform.kt

package com.example.kmpapptest

actual fun get_native_message(): String{
    return AndroidNativeLib.stringFromJNI()
}

class AndroidNativeLib {
    companion object {
        external fun stringFromJNI() : String
        init {
            System.loadLibrary("android-native-lib")
        }
    }
}
// shared/src/iosMain/kotlin/com/example/kmpapptest/Platform.kt

package com.example.kmpapptest
import kotlinx.cinterop.toKString

@kotlinx.cinterop.ExperimentalForeignApi
actual fun get_native_message(): String{
    return iosNativeLib.get_message()?.toKString().toString()
}

Kotlin通过JNI在Android环境调用C/C++方法

再来回顾下 AndroidNativeLibstringFromJNI方法


package com.example.kmpapptest

class AndroidNativeLib {
    companion object {
        external fun stringFromJNI() : String
        init {
            System.loadLibrary("android-native-lib")
        }
    }
}

在C/C++侧,对应的方法名为Java_com_example_kmpapptest_AndroidNativeLib_00024Companion_stringFromJNI

kotlin方法名和C/C++方法的对应规则:
1、Java_包名_类名_方法名
2、Java_包名_方法名
3、Java_包名_类名_00024Companion_方法名
"00024Companion" 是 "$Companion" 的转译字符串

shared//src/main/cpp/main.cpp中添加对应方法

//shared/src/main/cpp/main.cpp

#include <jni.h>

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_kmpapptest_NativeLib_00024Companion_stringFromJNI(JNIEnv * env , jobject thiz) {
    return env->NewStringUTF("test_kotlin_call_android_native");
}

修改shared/src/main/cpp/CMakeLists.txt

...
project("android-native-lib") # 这里的名字要和AndroidNativeLib类加载的lib库名对应
...

到这里,可以运行 androidApp看下效果

Kotlin通过Cinterop在iOS环境调用C/C++方法

shared/main/创建cinterop/CMakeLists.txt,cinterop/ios_cmake.shcinterop/ios.cpp,cinterop/ios.hcinterop/ios.def

shared
        |_src
                |_main (C/C++方法)
                    |_cinterop
                        |_CMakeLists.txt (ios lib 构建配置)
                        |_ios_cmake.sh (cmake构建具体命令)
                        |_ios.h (方法声明)
                        |_ios.cpp (方法实现)
                        |_ios.def (暴露给kotlin的方法定义文件)

方法声明和实现

//shared/src/main/cinterop/ios.h

#ifndef TEST_SHARED_IOS_H
#define TEST_SHARED_IOS_H

#if defined(__cplusplus)
extern "C" {
#endif

const char *get_message(void);

#if defined(__cplusplus)
}
#endif

#endif //TEST_SHARED_IOS_H
//shared/src/main/cinterop/ios.cpp

#include "ios.h"

extern "C"  const char *get_message(void)
{
    return "cinterop_test_kotlin_call_ios_native";
}

CMakeLists.txt 和 ios_cmake.sh

编辑shared/src/main/cinterop/CMakeLists.txt

# shared/src/main/cinterop/CMakeLists.txt

# Sets the minimum CMake version required for this project.
cmake_minimum_required(VERSION 3.22.1)

# 编译模块名 产物为libnative-lib
project("native-lib")

# 设置系统环境
set(CMAKE_SYSTEM_NAME iOS)
# 设置cpu arch
set(CMAKE_OSX_ARCHITECTURES arm64 x86_64)

# 指定头文件目录
include_directories(${CMAKE_SOURCE_DIR})

# 收集所有 源文件
file(GLOB SOURCES
        ${CMAKE_SOURCE_DIR}/*.cpp
)

add_library(${CMAKE_PROJECT_NAME} STATIC
        # List C/C++ source files with relative paths to this CMakeLists.txt.
        ${SOURCES})

编辑shared/src/main/cinterop/ios_cmake.sh

# 产物输出目录 (外面传入参数)
build_output_dir=$1

# 根据CMakeLists.txt生产构建配置信息,如makefile
cmake . -B $build_output_dir

cd $build_output_dir
#执行构建
cmake --build .

可以手动执行ios_cmake.sh看下效果,执行后记得清理输出物

cd ios_cmake.sh目录
./ios_cmake.sh  ./buildout

.def文件

暴露给kotlin的方法定义文件
内容如下:

# shared/src/main/cinterop/ios.def

# 暴露给kotlin的包名
package = iosNativeLib

# 暴露给kotlin的头文件,kotlin侧可以看到里面的方法名
# 这里的路径是相对于.def文件的路径
headers = ios.h

# 构建过程中,链接库的搜索目录
# 这里的路径是相对于projectdir(gradle的这个设定是不是bug?)
# 也可以使用绝对路径,从工程管理角度不建议用绝对路径
libraryPaths = build/cmake

# 需要链接的库文件,这里需要链接上面的iOS nativle lib
# 查找libraryPaths目录下的库文件
staticLibraries = libnative-lib.a

修改Gradle脚本

上面已经完成了ios native lib的构建,以及native lib需要暴露的方法配置,现需要使用Gradle脚本将这些工作串起来。

// shared/build.gradle.kts

fun org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget.addCInterop_sharedCC() {
    val main by compilations.getting // target name : main or test
    val iosNativeLib by main.cinterops.creating {
        // .def 定位文件
        defFile("src/main/cinterop/ios.def") 
        // kotlin侧需要搜索的头文件目录
        includeDirs("src/main/cinterop")
    }
}

kotlin {
    ...

    iosX64() {
        addCInterop_sharedCC()
    }
    iosArm64()  {
        addCInterop_sharedCC()
    }
    iosSimulatorArm64() {
        addCInterop_sharedCC()
    }
    ...
}

...
// 创建ios_buildCmake任务
val ios_buildCmake by tasks.registering(Exec::class) {
    val ios_cmakebuildDir = File(projectDir ,"build/cmake")
    val ios_cmakeSourceDir = File(projectDir,"src/main/cinterop")
    description = "Build native C/C++ with CMake"
    workingDir = ios_cmakeSourceDir
    commandLine = listOf("sh", "ios_cmake.sh" , ios_cmakebuildDir.absolutePath)
}
# 设置cinterop的ios构建任务依赖ios_buildCmake任务
tasks.matching { it.name.startsWith("cinterop") && it.name.contains("Ios") }.all {
    dependsOn(ios_buildCmake)
}

至此,可以执行iOSApp看看效果

总结

在无法摆脱C/C++生态的情况下,使用Kotlin Multiplatfrom实现跨平台,维护成本比较高,不如直接使用C/C++。

发表回复

© Beli. All Rights Reserved.