[Kotlin/Native] 创建全平台动态库

Posted by rarnu on 05-28,2019

本来是想写一下用 Kotlin/Native 玩 JNI 的,同时手里有一些 Android JNI 项目试图做一个移植,研究了一下之后发现,Kotlin/Native 的跨平台库实在是太强大了,除了喊 666 也没别的话好说,于是干脆搞个大的,把全平台的库全部盘一遍。

首先要说一下,Kotlin/Native 体系下有一个很牛逼的命令叫做 konan,社区里也有人称它为柯南,这个全平台具体能全到什么程度,还要看柯南的。

$ konanc -list-targets
macos_x64:                    (default) macbook, macos, imac
ios_arm32:                    iphone32
ios_arm64:                    iphone, ipad, ios
ios_x64:                      iphone_sim
linux_x64:                    linux
linux_arm32_hfp:              raspberrypi
android_arm32:                          
android_arm64:                          
wasm32:      

除此之外,Kotlin/Native 原生支持 JSJVM 的编译,不被列在这个表内。另外,这个列表并非全部平台,有一些编译目标如 mingwX64 只能在 Windows 下进行,因此在 Mac 上的 konan 无法列出这些目标。

知道这些后,我们就可以按自己的需要来添加编译平台,这里需要注意的是,每个平台支持的编译方式都是有差异的,不能一概而论,下面我列了个表,来帮助大家写编译脚本:

平台target 名称编译目标特殊选项
jsjs moduleKind [可选 amd, commonjs, umd]
sourceMap [可选 true, false]
jvmjvm jvmTarget
iOSiosArm64frameworkembedBitcode [可选 bitcode]
staticLib
iosArm32frameworkembedBitcode [可选 bitcode]
staticLib
iosX64frameworkembedBitcode [可选 bitcode]
staticLib
AndroidandroidNativeArm64executableentryPoint
sharedLib
staticLib
androidNativeArm32executableentryPoint
sharedLib
staticLib
MacmacosX64executableentryPoint
sharedLib
staticLib
frameworkembedBitcode [可选 bitcode]
LinuxlinuxX64executableentryPoint
sharedLib
staticLib
linuxMipsel32executableentryPoint
sharedLib
staticLib
linuxMips32executableentryPoint
sharedLib
staticLib
WindowsmingwX64executableentryPoint
sharedLib
staticLib
WebAssemblywasm32executableentryPoint
Raspberry PilinuxArm32HfpexecutableentryPoint
sharedLib
staticLib

这个表怎么用呢?比如说要针对 iOS Arm64 编译一个 bitcode 的 framework,参考上表可以这样写脚本:

kotlin {
    iosArm64("ios64") {
        binaries {
            framework {
                embedBitcode "bitcode"
            }
        }
    }
}

同样的,如果要针对 Android Arm64 编译 JNI 库和对应的静态库,只需要这样写:

kotlin {
    androidNativeArm64("android64") {
        binaries {
            sharedLib { }
            staticLib { }
        }
    }
}

下面来说一下使用动态库的问题,对于通常的 C/C++ 库来说,Kotlin 都可以简单的通过 cinterop 来引入,官方也早就相关的文档来说明(点击查阅),同样的,也可以在 C/C++ 里使用 Kotlin 的库,官方文档也说明了这一点(点击查阅)。

在这里有一个问题,如官方文档所述,在实际应用中为了一个函数去写一大堆代码显然是不合适的,这里贴来官方的例子比较一下:

#include "libnative_api.h"
#include "stdio.h"

int main(int argc, char** argv) {
  //obtain reference for calling Kotlin/Native functions
  libnative_ExportedSymbols* lib = libnative_symbols();
... ...
  //use C and Kotlin/Native strings
  const char* str = "Hello from Native!";
  const char* response = lib->kotlin.root.example.strings(str);
  printf("in: %s\nout:%s\n", str, response);
  lib->DisposeString(response);
... ...
  return 0;
}

如果我想直接使用里面的 strings() 方法要怎么办呢?其实是可以使用命名注解的:

package example
... ...
@CName("strings")
fun strings(str: String) : String? {
  return "That is '$str' from C"
}
... ...

注意此处的 @CName() 对应的名称,就是最终导出的名称,所以我们就可以用简单的办法来访问了:

#include "libnative_api.h"
#include "stdio.h"

int main(int argc, char** argv) {
  //obtain reference for calling Kotlin/Native functions
  // libnative_ExportedSymbols* lib = libnative_symbols();
... ...
  //use C and Kotlin/Native strings
  const char* str = "Hello from Native!";
  // const char* response = lib->kotlin.root.example.strings(str);
  const char* response = strings(str);
  printf("in: %s\nout:%s\n", str, response);
  // lib->DisposeString(response);
... ...
  return 0;
}

通过同样的方法,我们可以声明 JNI 的导出函数:

@CName("Java_com_rarnu_sample_NativeAPI_sayHello")
fun sayHello(env: CPointer<JNIEnvVar>, thiz: jobject) { ... ... }

另外,全网搜索 Kotlin 调用自己的库未果,而且经过一系列常规的尝试后发现两个大家都遇到了的问题(第一个第二个),显然不可能往这些方向继续进行。

其实 Kotlin 要使用自己的库并没有那么麻烦,只不过设计思路有些不同,因为有一个很神奇的中间层叫 klib,通过引用 klib 就可以

$ konanc sample.kt -p library -o sample

编译库文件源码,可以得到一个 sample.klib 把它放到项目里,然后直接引用文件即可:

sourceSets {
    ... ...
    macosMain {
        dependencies {
            implementation files('sample.klib')
        }
    }
}

这样一来,这个 klib 就可以正确的代码中引用到了。