为 Compose MultiPlatform 添加 C/C++ 支持(1):在 kotlin 中使用 cinterop 实现与 C/C++ 互操作

前言

在安卓中我们可以使用 jvm 提供的 jni 方便的编写 C/C++ 代码并与 java/kotlin 互操作。

但是当使用 kotlin MultiPlatform 时想要调用 C/C++ 代码将变得麻烦甚至是不可用,因为对于 Android 和 Desktop 来说依旧使用的是 jvm ,所以只要稍微适配一下也不是不能用。但是如果涉及到使用 kotlin native 的平台,比如 iOS,那么就无法再使用 jvm 的 jni 了。

此时,我们只能使用 kotlin 提供的 cinterop 实现与 C/C++ 的互操作。

只是这样又带来一个问题,那就是由于 Android 和 Desktop 平台使用的是 jvm,所以 cinterop 又不太好使了。

因此为了实现全平台的 C/C++ 互操作,我们需要使用 kotlin 的 expectactual 分别适配不同平台的互操作。

因为 jvm 平台使用 jni 比较简单,相信各位安卓开发都有使用过,所以在第一部分我们将首先介绍非 jvm 平台的 cinterop 。

万事都得从头开始,不要妄想一口吞一个大胖子,所以本文我们将从简单的开始,以 Desktop 端举例如何配置及使用 cinterop 。

注意:有一点需要明确的是,上文中我们既说 Desktop 是 jvm 实现,又说要使用 Desktop 举例使用 cinterop。这是因为 Compose Desktop 使用的是 jvm 实现,不支持 native,也就不支持 cinterop,但是单纯的 Kotlin Desktop 程序(非 Compose)是支持使用 native 的,所以可以使用 Desktop 举例使用 cinterop 。

Hello,World

Hello,World 是我们程序员永远的第一次,所以这次我们也以一个 Hello World 作为我们的示例项目。

首先,在 intelliJ 中新建一个项目 Kotlin Multiplatform - Native Application

1.jpg

接着在项目的 src 目录新建一个 nativeInterop/cinterop 目录,这个目录名称也可以是任意名称,而 nativeInterop/cinterop 是默认配置目录。

在创建好的 nativeInterop/cinterop 新建一个 libtest.h 文件。

在该文件中定义我们需要暴露给 kt 调用的函数:

#ifndef LIB2_H_INCLUDED
#define LIB2_H_INCLUDEDchar* get_message(char* name);#endif

这里我们定义了一个函数 get_message 接收一个字符数组(字符串)参数 name 并返回一个字符数组。

然后,再在这个目录新建一个 libtest.def 文件用于映射刚才的 .h 文件和 kt 函数:

headers = libtest.h

headers 参数用于指明需要映射的头文件,这里我们指向了相同目录的 libtest.h 文件。

完成后的目录结构应该是这样的:

2.jpg

现在,我们可以在 kt 文件中调用刚才声明的 get_message 函数了,在 Main.kt 文件中:

@OptIn(ExperimentalForeignApi::class)
fun main() {println(get_message("world and equationl".cstr)?.toKString())
}

此时,Main.kt 文件应该会报错找不到 get_message ,先不急,我们先接着配置。

在项目的 build.gradle.kts 文件中,找到 kotlin 代码块下的 nativeTarget 代码块,并在其中添加如下代码:

kotlin {// ……nativeTarget.apply {compilations.getByName("main") {cinterops {val libtest by creating}}// ……}
}

这样,cinterop 会在默认目录查找和 libtest 同名的 def 文件进行编译。

当然,也可以自定义参数:

kotlin {// ……nativeTarget.apply {compilations.getByName("main") {cinterops {val libtest by creating {defFile(project.file("src/nativeInterop/cinterop/libtest.def"))compilerOpts("-Isrc/nativeInterop/cinterop")}}}// ……}
}

其中,defFile 参数指定了 def 文件的位置;compilerOpts 参数指定了需要的编译选项。

修改完成后 sync 一下 gradle,然后返回 Main.kt 文件,现在可以看到 IDE 已经提示可以导入 get_message 了,导入后文件不再报错:

3.jpg

需要注意的是,此时直接运行是运行不了的,因为刚才我们只是定义了 get_message 函数,但是并没有写具体的实现。

现在我们需要写上这个函数的实现,修改 libtest.def 文件为:

headers = libtest.h---#include <string.h>char* get_message(char* name) {char *greeting = "hello, ";char* message = (char *) malloc(strlen(greeting) + strlen(name));strcpy(message, greeting);strcat(message, name);return message;
}

没错,我们可以直接在 def 文件下方添加具体的代码,只要将代码和配置信息使用三个横杠 --- 隔开即可。

上述的 C 代码非常简单,就是把接收到的 name 参数和 "hello, " 字符串拼接后再返回。

现在,我们再来运行 Main.kt,结果如下:

4.jpg

可以看到输出完美符合预期。

在上述的 get_message 实现我们是直接写在了 def 文件中,事实上,def 文件中的代码在编译时最终还是会附加到配置的 .h 文件末尾,也就是说,我们完全可以直接把代码写到 .h 文件中,这样还能有代码高亮和代码提示,直接写在 def 文件中的话,代码就是个普通文本,对查看和修改代码都很不方便。

但是有一点需要注意,cinterop 只有在 def 文件发生改变了才会重新编译,换句话说,如果不把代码写在 def 文件中的话,每次修改都需要手动执行 gradlew clean 清除编译缓存后再运行,否则修改不会被重新编译。

咱也不知道这是个 BUG 还是个 feature 啊,反正我查资料的时候看到早在 2021 年就有人在 jetbrains 的 youtrack 上反馈类似的问题了,当时官方回复是已记录该问题,但是事实证明两年过去了这个问题依旧存在。

不管怎么说,为了让代码更好看,我们还是把具体实现单独抽出到一个 .c 文件 libtest.c 中吧:

#include <string.h>char* get_message(char* name) {char *greeting = "hello, ";char* message = (char *) malloc(strlen(greeting) + strlen(name));strcpy(message, greeting);strcat(message, name);return message;
}

然后在 libtest.h 中引入这个文件:

#ifndef LIB2_H_INCLUDED
#define LIB2_H_INCLUDEDchar* get_message(char* name);#include<libtest.c>#endif

最后需要注意的一点是,cintrtop 映射到 kt 函数只支持 C 不支持 C++,但是这并不意味着就无法使用 C++,只要把 C++ 再使用 C 包装一遍,然后暴露给 kt 即可。

接下来,我们简单介绍下 kt 和 c 之间的数据互相映射。

kt 与 c 的数据映射

基本数据类型映射

先上一个结论表格,方便大家查阅:

Ckotlin
charByte
unsigned charUByte
shortShort
unsigned shortUShort
intInt
unsigned intUInt
longLong
unsigned longULong
floatFloat
doubleDouble

根据上述表格我们已经可以一目了然的看出来 C 中各个基本数据类型会转换为 kotlin 中的何种类型,但是光说不做假把式,我们写一个小 demo 来实际验证一下。

因为我们只需要查看数据类型的映射,所以不需要编写具体的代码实现,直接声明函数就行,因此我们直接修改上一节中的 libtest.h 文件:

char data_char(char a);
unsigned char data_u_char(unsigned char a);
short data_short(short a);
unsigned short data_u_short(unsigned short a);
int data_int(int a);
unsigned int data_u_int(unsigned int a);
long data_l_long(long a);
unsigned long data_u_l_long(unsigned long a);
float data_float(float a);
double data_double(double a);

重新编译后生成对应的 kotlin 函数,查看自动生成的函数实现如下:

@kotlinx.cinterop.internal.CCall public external fun data_char(a: kotlin.Byte): kotlin.Byte { /* compiled code */ }@kotlinx.cinterop.internal.CCall public external fun data_double(a: kotlin.Double): kotlin.Double { /* compiled code */ }@kotlinx.cinterop.internal.CCall public external fun data_float(a: kotlin.Float): kotlin.Float { /* compiled code */ }@kotlinx.cinterop.internal.CCall public external fun data_int(a: kotlin.Int): kotlin.Int { /* compiled code */ }@kotlinx.cinterop.internal.CCall public external fun data_l_long(a: kotlin.Long): kotlin.Long { /* compiled code */ }@kotlinx.cinterop.internal.CCall public external fun data_short(a: kotlin.Short): kotlin.Short { /* compiled code */ }@kotlinx.cinterop.internal.CCall public external fun data_u_char(a: kotlin.UByte): kotlin.UByte { /* compiled code */ }@kotlinx.cinterop.internal.CCall public external fun data_u_int(a: kotlin.UInt): kotlin.UInt { /* compiled code */ }@kotlinx.cinterop.internal.CCall public external fun data_u_l_long(a: kotlin.ULong): kotlin.ULong { /* compiled code */ }@kotlinx.cinterop.internal.CCall public external fun data_u_short(a: kotlin.UShort): kotlin.UShort { /* compiled code */ }

可以看出,映射关系就是上述表格中的关系。

只是这里需要额外注意的一点是关于字符串的映射关系,在上文中我们提到过,cinterop 只支持 C,而在 C 中是没有字符串这种类型的。

一般来说,在 C 中我们是使用一个字符数组 char string[]char* string 来表示字符串。

那么,C 和 kotlin 又是怎么映射字符串或者说字符数组的呢?

还是一样的,我们直接修改 libtest.h 定义一个函数来看:

char* get_string(char* string);

重新编译后生成的 kotlin 函数如下:

@kotlinx.cinterop.internal.CCall public external fun get_string(string: kotlinx.cinterop.CValuesRef<kotlinx.cinterop.ByteVar /* = kotlinx.cinterop.ByteVarOf<kotlin.Byte> */>?): kotlinx.cinterop.CPointer<kotlinx.cinterop.ByteVar /* = kotlinx.cinterop.ByteVarOf<kotlin.Byte> */>? { /* compiled code */ }

可以看到,该函数的参数值类型为 kotlinx.cinterop.CValuesRef<kotlinx.cinterop.ByteVar /* = kotlinx.cinterop.ByteVarOf<kotlin.Byte> */>?

而返回值为 kotlinx.cinterop.CPointer<kotlinx.cinterop.ByteVar /* = kotlinx.cinterop.ByteVarOf<kotlin.Byte> */>?

这两个类型看起来有点长,好像也不是什么基本数据类型,那么,要怎么使用呢?

其实也很简单,对于参数值的话,我们直接使用字符串,然后用 cinterop 的 cstr 扩展函数转换即可;而返回值的话同理,直接使用扩展函数 toKString() 转为 kotlin 的 string 即可。

对于上面定义的 get_string 函数,我们可以这样用:

println(get_string("bye, monkey fish".cstr)?.toKString())

其他数据映射

首先我们来了解一下对于数组的映射。

这里我们以 int 类型的数组举例,依旧是在 libtest.h 中定义函数如下:

int* int_array(int* ints);

生成的函数如下:


@kotlinx.cinterop.internal.CCall public external fun int_array(ints: kotlinx.cinterop.CValuesRef<kotlinx.cinterop.IntVar /* = kotlinx.cinterop.IntVarOf<kotlin.Int> */>?): kotlinx.cinterop.CPointer<kotlinx.cinterop.IntVar /* = kotlinx.cinterop.IntVarOf<kotlin.Int> */>? { /* compiled code */ }

可以看到参数类型还有返回值和上一节的字符串一样,只是泛型从 Byte 变为了 Int。

是的,这确实是如此,因为上面我们就说过了,在 C 中没有字符串,所谓字符串其实就是字符数组。

那么,即然字符数组有 cstrtoKString() 扩展函数,是不是其他类型也有类似的扩展函数呢?

诶,你猜怎么着,还真有。

例如上面的 int 数组,使用时可以这样:

val newList = int_array(intArrayOf(1, 2, 3).toCValues())
val firstValue = newList!![0]

需要注意的是,如果是 C 中需要的参数是 int 数组,则在 kotlin 中也只能使用 IntArray,然后使用 toCValues() 扩展函数转换。

而返回值 CPointer<IntVar> 其实就可以直接当成一个普通的 Int 数组来使用,在上例中 firstValue 的类型就是 Int 。

关于其他类型的数据映射我们这里就不再赘述,有兴趣的可以自行查阅官方文档:

  1. 结构体和联合体
  2. 函数指针

总结

自此我们已经能够大致了解了如何在 kotlin native 中使用 cinterop 和 C/C++ 交互,虽然我全文举例都只是在 Desktop 平台举例,但是实际上对于同样使用 kotlin native 的 iOS 平台也是一样的用法。

只需要将 def 文件的配置放到 iOS 相关的 gradle 配置下即可,例如:

iosArm64().apply {compilations.getByName("main") {cinterops {val libtest by creating {defFile(project.file("src/nativeInterop/cinterop/libtest.def"))compilerOpts("-Isrc/nativeInterop/cinterop")}}}
}

其余地方和 Desktop 没有任何区别。

下一章我们将介绍如何在 Compose MultiPlatform 中为 Desktop 和 Android 添加 jni 支持。

参考资料

  1. Interoperability with C
  2. Kotlin / Native — How to use C in Kotlin

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/214523.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

网络攻击(三)--攻击阶段

5. 威胁建模阶段 目标 了解威胁建模阶段的工作内容 工作内容 威胁建模主要使用在情报搜集阶段所获取到的信息&#xff0c;来标识出目标系统上可能存在的安全漏洞与弱点。 在进行威胁建模时&#xff0c;确定最为高效的攻击方法、所需要进一步获取到的信息&#xff0c;以及从…

048:利用vue-video-player播放m3u8

第048个 查看专栏目录: VUE ------ element UI 专栏目标 在vue和element UI联合技术栈的操控下&#xff0c;本专栏提供行之有效的源代码示例和信息点介绍&#xff0c;做到灵活运用。 &#xff08;1&#xff09;提供vue2的一些基本操作&#xff1a;安装、引用&#xff0c;模板使…

AWS Ubuntu设置DNS解析(解决resolve.conf被覆盖问题)

众所周知&#xff1a; Ubuntu在域名解析时&#xff0c;最直接使用的是/etc/resolve.conf文件&#xff0c;它是/run/systemd/resolve/resolve.conf的软链接&#xff0c;而对于刚装完的ubuntu系统&#xff0c;该文件的内容如下 ubuntuip-172-31-36-184:/etc$ cat resolv.conf #…

【MYSQL】单表查询

查询语法&#xff1a; select 字段&#xff08;*表示全字段&#xff09; from 数据表 【where 条件表达式】 【group by 分组字段【having 分组条件表达式】】 【order by 排序字段【asc | desc】】 例子&#xff1a; 教职工表Teacher(Tno, TName, age, sal, mgr, DNo)&#…

【原创创新点】金属工件缺陷检测系统:Efficient Multi-Scale-Conv的改进YOLOv8

1.研究背景与意义 项目参考AAAI Association for the Advancement of Artificial Intelligence 研究背景与意义&#xff1a;金属工件是现代工业生产中不可或缺的重要组成部分。金属工件的质量和性能直接影响到产品的品质和效率&#xff0c;因此对金属工件的研究和改进具有重要…

【K8S in Action】服务:让客户端发现pod 并与之通信(1)

服务是一种为一组功能相同的 pod 提供单一不变的接入点的资源。当服务存在时&#xff0c;它的 IP 地址和端口不会改变。 客户端通过 IP 地址和端口号建立连接&#xff0c; 这些连接会被路由到提供该服务的任意一个 pod 上。 pod 是短暂&#xff0c;会删除增加&#xff0c;调度…

【go语言开发】go项目打包成Docker镜像,包括Dockerfile命令介绍、goctl工具生成

本文主要介绍如何将go项目打包成镜像&#xff0c;首先介绍Dockerfile常用命令介绍&#xff0c;然后介绍使用工具goctl用于生成Dockerfile&#xff0c;还可以根据需求自定义指令内容&#xff0c;最后讲解如何将go-blog项目打包成镜像&#xff0c;以及如何运行等 文章目录 前言Do…

JavaWeb(十)

一、JavaWeb概述 Web&#xff1a;全球广域网&#xff0c;也称为万维网(www)&#xff0c;能够通过浏览器访问的网站。 JavaWeb&#xff1a;使用 Java技术进行web互联网开发。 二、JavaWeb 技术栈 2.1、B/S 架构 B/S 架构&#xff1a;Browser/Server&#xff0c;浏览器/服务器…

ELK的日志解决方案

ELK的日志解决方案 ELK是什么 ELK 是一个缩写&#xff0c;代表 Elastic Stack&#xff0c;而不是三个独立的产品名称。Elastic Stack 是一个开源的数据处理和分析平台&#xff0c;用于实时搜索、分析和可视化大规模数据。ELK 是由三个主要的组件构成&#xff1a; Elasticsea…

[MySQL--进阶篇]存储引擎的体系结构、简介、特点、选择

前言 ⭐Hello!这里是欧_aita的博客。 ⭐今日语录&#xff1a;不要在乎别人怎么看你&#xff0c;因为他们根本就没有时间&#xff0c;他们只关心他们自己。 ⭐个人主页&#xff1a;欧_aita ψ(._. )>⭐个人专栏&#xff1a; 数据结构与算法 MySQL数据库 存储引擎 前言MySQL体…

泽攸科技桌面型扫描电子显微镜(SEM)技术解析

台式扫描电子显微镜是一种利用电子束扫描样品表面并检测样品反射或发射的电子信号&#xff0c;从而获得样品表面形貌、结构和成分信息的仪器。它的工作原理是由电子枪发出的电子束经过栅极静电聚焦后成为直径50微米的点光源&#xff0c;然后在加速电压作用下&#xff0c;经两三…

分类预测 | SSA-HKELM-Adaboost麻雀算法优化混合核极限学习机的数据分类预测

分类预测 | SSA-HKELM-Adaboost麻雀算法优化混合核极限学习机的数据分类预测 目录 分类预测 | SSA-HKELM-Adaboost麻雀算法优化混合核极限学习机的数据分类预测分类效果基本描述程序设计参考资料 分类效果 基本描述 1.SSA-HKELM-Adaboost麻雀算法优化混合核极限学习机的数据分类…

【Spring Boot 源码学习】ApplicationListener 详解

Spring Boot 源码学习系列 ApplicationListener 详解 引言往期内容主要内容1. 初识 ApplicationListener2. 加载 ApplicationListener3. 响应应用程序事件 总结 引言 书接前文《初识 SpringApplication》&#xff0c;我们从 Spring Boot 的启动类 SpringApplication 上入手&am…

我对迁移学习的一点理解——领域适应(系列3)

文章目录 1. 领域适应&#xff08;Domain Adaptation&#xff09;的基本概念2.领域适应&#xff08;Domain Adaptation&#xff09;的目标3.领域适应&#xff08;Domain Adaptation&#xff09;的实现方法4.领域适应&#xff08;Domain Adaptation&#xff09;的可以解决的问题…

RC522(RFID射频模块)读卡ID的简单应用

文章目录 一、RFID是什么&#xff1f;二、RC522模块三、使用步骤1.硬件1.硬件连接2.引脚定义 2.软件1.初始化配置代码如下&#xff08;示例&#xff09;&#xff1a;2.引脚配置代码如下&#xff08;示例&#xff09;&#xff1a;3.模块复位代码如下&#xff08;示例&#xff09…

芯片量产导入知识

什么是芯片量产 从芯片功能设计到生产制造、测试等环节&#xff0c;每一个环节都至关重要。 对于保障大规模发货后芯片指标表现的一致性&#xff0c;以及产品应用生命周期内的稳定性和可靠性&#xff0c;需要考虑多种因素。以下是一些相关的观点&#xff1a; 可量产性设计&am…

蓝桥杯2021年5月青少组Python程序设计国赛真题

30 个人在一条船上,超载&#xff0c;需要 15 人下船于是人们排成一队&#xff0c;排队的位置即为他们的编号。报数,从1开始,数到9的人下船。如此循环,直到船上仅剩15 人为止&#xff0c;问都有哪些编号的人下船了呢? 2】判断101-200之间有多少个素数&#xff0c;并输出所有素数…

零基础一看就会?Python实现性能自动化测试竟然如此简单

一、思考❓❔ 1.什么是性能自动化测试? 性能 系统负载能力超负荷运行下的稳定性系统瓶颈自动化测试 使用程序代替手工提升测试效率性能自动化 使用代码模拟大批量用户让用户并发请求多页面多用户并发请求采集参数&#xff0c;统计系统负载能力生成报告 2.Python中的性能自动化…

TCP数据粘包的处理

TCP数据粘包的处理 背锅侠TCP解决方案2.1 发送端2.2 接收端 背锅侠TCP 在前面介绍套接字通信的时候说到了TCP是传输层协议&#xff0c;它是一个面向连接的、安全的、流式传输协议。因为数据的传输是基于流的所以发送端和接收端每次处理的数据的量&#xff0c;处理数据的频率可…

周周爱学习之Redis重点总结

redis重点总结 在正常的业务流程中&#xff0c;用户发送请求&#xff0c;然后到缓存中查询数据。如果缓存中不存在数据的话&#xff0c;就会去数据库查询数据。数据库中有的话&#xff0c;就会更新缓存然后返回数据&#xff0c;数据库中也没有的话就会给用户返回一个空。 1.缓…