Kotlin Native CInterop
最近在搞 k/n 鸿蒙的适配,也是有一些前人踩过坑,但是都没有开源,还是要自己埋头钻研
这部分后续会给一个详细文档吧
今天主要来介绍 kotlin/native 中的 cinterop
其实,在编译 kotlin/native 编译器,生成编译器需要的 platformLib 时,我就已经看到了 cinterop。他通过 kotlin/native 工程中声明的一些 .def 文件,编译过后都变成了标准的 kotlin 的跨平台库(klib) 格式
cinterop 是一个 kotlin/native 提供的工具(源码也在,我们最终编译整个编译器后,dist/bin 下也会产生 cinterop 的可执行文件),他可以自动的产生一个 c/c++ lib 和 kotlin 之间的 binding(胶水代码),同时也是编译产出 kotlin 的跨平台库(klib) 的工具
klib 的基本格式如下(后续会讲解 .knm 和 .bc 这两个最重要的东西):
cinterop 效果简介
就以上图中的 platform.linux 提供给我们的能力为例
platform.linux 是 kotlin/native 编译器内部自带的,已经编译好了的 klib,其依赖为 linux 声明的 linux.def 生成
有了这个 klib,我们可以直接在 kotlin 中调用很多 linux 平台的函数,比如 epoll_create 等(下图就是部分 linux 的 kotlin 函数,同时这个文件就是上图 klib 中的 .knm 文件解析以后的样子)
这时候,就应该有一个非常大的疑问:
这些 kotlin 代码是怎么生成出来的?
cinterop 原理
由上面我们知道,cinterop 生成的 kotlin/c 的胶水代码其实就在 .knm 文件中,有了这个文件,我们就可以在 kotlin 世界中,import platform.linux,然后调用 epoll 函数
epoll 或者说 linux 平台的相关函数,都声明在各自的头文件中
下面是 linux.def 文件,它告知了需要导出哪些头文件的内容给 kotlin 调用
// linux.def
package = platform.linux
headers = aio.h aliases.h a.out.h argp.h argz.h byteswap.h cpio.h crypt.h \
elf.h endian.h envz.h error.h execinfo.h features.h fmtmsg.h \
fpu_control.h \
fstab.h _G_config.h gconv.h glob.h gnu-versions.h \
gshadow.h ieee754.h ifaddrs.h langinfo.h lastlog.h \
libintl.h libio.h link.h malloc.h mcheck.h mntent.h \
monetary.h mqueue.h \
nl_types.h nss.h obstack.h \
printf.h pty.h re_comp.h \
spawn.h stab.h stdio_ext.h syscall.h \
tar.h termio.h thread_db.h ttyent.h ulimit.h \
ustat.h utmpx.h values.h wait.h wordexp.h \
arpa/ftp.h arpa/inet.h arpa/nameser_compat.h \
arpa/nameser.h arpa/telnet.h arpa/tftp.h \
netinet/if_ether.h net/if_packet.h \
netinet/ether.h linux/in6.h netpacket/packet.h \
sys/acct.h sys/bitypes.h sys/cdefs.h \
sys/dir.h sys/epoll.h sys/errno.h \
sys/eventfd.h sys/fcntl.h \
sys/file.h sys/fsuid.h sys/gmon.h sys/gmon_out.h \
sys/inotify.h sys/kd.h sys/klog.h sys/mount.h \
sys/msg.h sys/mtio.h sys/param.h sys/pci.h \
sys/personality.h sys/prctl.h sys/procfs.h sys/profil.h \
sys/raw.h sys/reboot.h sys/resource.h \
sys/sem.h sys/sendfile.h sys/signalfd.h sys/signal.h \
sys/socket.h sys/socketvar.h sys/soundcard.h sys/statfs.h \
sys/statvfs.h sys/stropts.h sys/swap.h sys/syscall.h \
sys/sysctl.h sys/sysinfo.h sys/syslog.h sys/sysmacros.h sys/termios.h \
sys/timeb.h sys/timerfd.h sys/timex.h sys/ttychars.h \
sys/ttydefaults.h sys/types.h sys/ucontext.h sys/uio.h \
sys/ultrasound.h sys/un.h sys/unistd.h sys/user.h \
sys/ustat.h utime.h sys/utsname.h sys/vfs.h sys/vlimit.h \
sys/vt.h sys/vtimes.h sys/xattr.h
headers.x64 = stdc-predef.h uchar.h sys/auxv.h sys/fanotify.h
headers.arm64 = stdc-predef.h uchar.h sys/auxv.h sys/fanotify.h
compilerOpts = -D_ANSI_SOURCE -D_POSIX_C_SOURCE=199309 -D_BSD_SOURCE -D_XOPEN_SOURCE=700
depends = posix
那么,问题又来了,头文件又是如何一步一步生成 kotlin 代码的呢?
Clang 编译器
clang 可以用于解析 c/c++ 源码
那么,当接收到一个 .h 头文件后,cinterop 会使用 clang 编译器来解析 .h 文件(详情参考 org.jetbrains.kotlin.native.interop.indexer.NativeIndex.kt 周围关联的各种逻辑)
具体怎么做到的呢?
因为其依赖了 llvm,并且在编译器层面,给 llvm c++ api 建立了一层 jni,这样可以直接使用 kotlin 代码调用 llvm 函数,如 clang_createTranslationUnit 为例:
这样子,我们就可以在 kotlin 的世界中,使用 llvm 的函数(这里是运行在 java 环境下的标准 kotlin)
clang 有一系列的 api 可以用于解析源码文件,上述的 createTranslationUnit 就是一个类似 init 的 api,然后有各种解析方法声明、变量等等的方法(源码过多,可以详见 llvm index.h 头文件中的一些注释介绍 和 sample 代码)
通过,这一步,可以将 .h 全部转化为 clang ast
Kotlin Metadata
拿到 clang 解析完的 AST 后,我们需要进一步生成对应的 kotlin 描述
AST 会被整理到 NativeIndex 中,比如方法声明就存在了 functions 中, FunctionDecl 结构如下图:
但是,这里依然只是一个对于 c/c++ 方法的描述
还需要 StubIrBuilder 来生成 kotlin 的描述
这里还有一些对于 oc 的特殊处理,经过这一步以后,类型和声明相关的描述已经摆脱了 c/c++ clang 的世界,此时得到的是 StubIrElement(FunctionStub impl StubIrElement)
这里,其实对应的 kotlin 方法的描述已经生成,但是只是一个中间描述,还不是标准的 kotlin 描述。比如下图中的 方法名、方法参数、返回类型等等,已经存在
之后,通过 StubIrMetaDataEmitter 将这些 StubIrElement 又做了一层转换,转换为了 kotlin metadata,这里就是标准的 kotlin 世界了。比如 FunctionStub 会被转为 KmFunction
最终,会序列化变为我们 klib 图中的 .knm 文件
TODO: 调用 .knm 文件中生成的方法,如何编译的时候不报错的?
cstub.bc
.bc 文件是 llvm ir 生成的 bitcode
为什么需要 .bc?
因为我们在 kotlin 调用 c 方法时,实际有一层中间的胶水代码,这部分代码会被编译为 bitcode
我们可以使用 llvm-dis 查看,会发现我们例子中的 epoll_create 相关的方法声明 epoll_create_warpper263(下图是 linux platform 的 cstub.bc 的部分内容)
这里其实有个小疑问,为啥是 epoll_create_wrapper263?
围绕这个诡异的 wrapper 我们还发现了一个函数指针
@knifunptr_platform_linux273_epoll_create = local_unnamed_addr global i8* bitcast (i32 (i32)* @platform_linux_epoll_create_wrapper263 to i8*), align 8,这又是啥?
其实,我们在 .knm 中看到的 epoll_create,最终调用的就是 knifunptr_platform_linux273_epoll_create,然后 knifunptr_platform_linux273_epoll_create 其实就是一个函数指针调用到 wrapper263,wrapper263 进一步调用了真正的 epoll_create
而且,wrapper263 是一个内联函数,最终应该是会被优化掉的
所以,最终我们使用 klib 产生目标平台的二进制时,写完的 kotlin 代码(包括对于 c/c++ lib 的一些调用),都会被编译器解析变成 kotlin backend ir,然后变成 llvm ir ,生成 bitcode,然后和我们胶水代码的 bitcode 还有 k/n runtime 相关的 bitcode 合并,最终生成一个对应平台的二进制文件
而当我们只是产生 klib 的时候,只会生成 klib 的标准格式(比较重要的是 .bc 和 .knm)
总结
如下图(c 暴露给 kotlin 视角)