kotlin/native_kotlin / native - CSDN
  • 作者简介禹昂,携程移动端高级工程师。Kotlin 中文社区核心成员,官方文档译者。一、前言作为 Kotlin Multiplatform 体系重要组成部分之一的 Kotlin/Nati...

    作者简介

     

    禹昂,携程移动端高级工程师。Kotlin 中文社区核心成员,官方文档译者。


    一、前言

    作为 Kotlin Multiplatform 体系重要组成部分之一的 Kotlin/Native ,目前还是一项处于 beta 阶段的技术。而 Kotlin/Native与 Kotlin/JVM 的异步并发模型也有着极大的不同,因此如果要实践 Kotlin Multiplatform,则事先对 Kotlin/Native的异步并发模型进行探究就显得很有必要。

    相较于 Kotlin/Native,Kotlin/JVM 也许为更多的人所熟知。基于 JVM 的异步并发机制,Kotlin/JVM 提供了通过编译器与线程池实现的协程来完成异步并发任务。Kotlin/JVM 的协程既能完成异步请求,也能完成并行计算,并且由于协程中拥有挂起(suspend),Kotlin/JVM 就可以在协程而非线程的层面上来解决并发竞争的问题。

    即当并发竞争出现的时候,这套机制只需将协程挂起而无需阻塞线程,而对于是否发生竞争的判断可以转移到原子操作上。这样的机制避免了 JVM重量级锁的出现,个人认为这确实是 Kotlin/JVM 的协程相对于传统 JDK 中异步并发 API 的一个优势(详见文末参考文档链接 1、2)。

    但 Kotlin/Native 程序作为一种原生二进制程序,相当于是重新开发的一门语言,由于没有现成的类似于 JVM 提供的异步并发机制作为依赖,所以它必须实现一套自己的异步并发模型。由于 Kotlin 在编程范式上吸收了部分函数式编程的特性,因此 Kotlin/Native 的同步方案从设计思想上向函数式编程靠拢,即对象不变性,其宗旨就是如果对象本身不可变,那就不存在线程安全的问题。

    Kotlin/Native 用于实现异步和并发的方案主要有三种。

    1)基于宿主环境(操作系统)实现。例如与使用 POSIX C 编写原生程序一样。直接使用相关操作系统平台提供的 API 来自己开启线程,在 POSIX 标准的系统上,手动调用 pthread_create函数来创建线程。但是这样的代码实现违反了平台通用性的原则,例如,如果你要将你的程序移植到非 POSIX 标准的系统上,那异步并发方式就得全部改用相关平台的机制,可移植性太差,在编写多平台程序的时候这种方式基本上是行不通的。

    2)Kotlin/Native 自身提供给了我们两套异步并发的 API,首先是协程,但 Kotlin/Native 的协程与 Kotlin/JVM的协程区别很大,Kotlin/Native 的协程是单线程的,也就是说它只能用来执行一些不占用 CPU 资源的异步并发任务,例如网络请求。但如果要利用CPU 多核的能力来进行并行计算,Native 版的协程就失去了作用,当然,官方说了要尽快解决这个问题,并且于 2019 年 12月中已经发布了 Native 多线程版协程的预览版本,这个会在后文详细讨论。

    3)除了协程之外,官方在 Kotlin/Native 诞生之初就已经提供了另一套专门做并行任务的工具,即 Worker 。Worker 与 Kotlin/Native 的异步并发模型紧密相连,做到了既能利用 CPU 多核能力,又能保障线程安全(虽然做法略微粗暴)。这篇文章我们会先介绍基于 Worker 与对象子图的现有异步并发模型,最后再讨论当前预览版本的多线程协程。

    注意,本文基于 Kotlin 1.3.61,Kotlin/Native 作为一个实验性项目,任何的版本变动都有可能造成 API 的破坏性变更。

    二、原生并发模型:Worker 与对象子图(Subgraph)

    这部分内容,官方文档较少,目前仅有一篇(见参考链接 3),而且其内容有一定滞后性,所以本文中的部分结论可能会与该文档不符,期待后续官方更新。

    Worker 与线程类似,通过打印线程 id 进行验证发现,一个 Worker 基本对应一个线程。在编写程序时,如果需要开启线程,就应该创建一个 Worker 。Kotlin/Native 对跨线程/Worker 访问对象拥有严格的限制,因此对象在一定维度上又分为两种状态,即 Freeze(冻结)与 Unfreeze(非冻结)。

    冻结的对象是编译期即可证明为不可变的对象,或者是手动显式添加 @SharedImmutable 注解的对象,系统默认这类对象不可变,可以在任意的线程/Worker 中访问,而非冻结对象通常不可在创建它之外的线程/Worker 中访问。Kotlin/Native通过给对象生成对象子图(subgraph)的方式,然后在运行时遍历对象子图来检测是否发生了跨线程/Worker 访问。

    2.1 对象冻结

    首先创建一个基本的 Kotlin/Native 工程,本文基于 macOS 10.15.1。

    对象冻结,即一个对象被创建之后即与当前线程/Worker 绑定,在不加特殊标记的情况下,在其他线程/Worker 访问该对象(无论是读还是写)就会抛出异常。但是存在另外一种对象,它们在编译期即可被证明是不可变的,这类对象就被称为冻结的对象。因此冻结对象可以在任意线程内访问,目前冻结对象有:

    • 枚举类型

    • 不加特殊修饰的单例对象(即使用 object 关键字声明的)

    • 所有使用 val 修饰的原生类型变量与 String(这种情况也就包含了 const 修饰的常量)

    如果我们要将其他类型的全局变量/成员变量声明为冻结的,可以使用注解 @SharedImmutable,它可以让变量的多线程访问通过编译,但如果运行时发生了对该变量的修改,程序就会抛出 IncorrectDereferenceException 异常。除此之外,官方还表示之后可能会增加对象动态冻结的情况,也就是说一个对象一开始不是冻结的,但在运行时从某一刻开始,就变为一个冻结对象,但是无论如何,一个已被冻结的对象都是不能被解除冻结的。

    2.2 Worker 的基本用法

    下面我们来看看如何在 Kotlin/Native 中开启子线程进行异步计算。

    在 Kotlin/Native 中我们使用 Worker 来做这件事,一个 Worker 即代表一个线程(类 Unix 系统),但在用法上却接近 Java的 Future/Promise 或 Kotlin 协程中的 async/await。与传统的 Java 中使用 Thread 的多线程编程方式相比,Worker对参数的传入以及对执行结果的获取更为严格,下面看一个例子:

    fun main() {
        val worker = Worker.start(true, "worker1")
        println("Position 1, thread id: ${pthread_self()!!.rawValue.toLong()}")
        val future = worker.execute(TransferMode.SAFE, {
            println("Position 2, thread id: ${pthread_self()!!.rawValue.toLong()}")
            1 + 2
        }) {
            println("Position 3, thread id: ${pthread_self()!!.rawValue.toLong()}")
            (it + 100).toString()
        }
        future.consume {
            println("Position 3, thread id: :${pthread_self()!!.rawValue.toLong()}")
            println("Result: $it")
        }
    }
    

    使用 Worker.start 函数我们就可以创建一个新的 Worker,然后调用它的 execute函数就可以在别的线程执行任务了。这个函数接收三个参数,第一个是对象转移模式(后面会讨论),第二个参数将扮演一个生产者的角色(为了简便,后文我们使用源码中的命名 producer 来称呼它),它会在外面的线程执行,producer的返回值将在 execute 的第三个参数(也是个 lambda 表达式,同样,后文我们用源码中的命名 job 来称呼它)中作为参数来提供。

    而 job 中的代码会在别的线程中执行。最后 execute 函数的返回结果是一个 Future<T> 类型的对象,调用它的成员函数 consume即可在外部线程获得 job 执行的结果。

    为了验证代码中的几个关键位置到底是在哪个线程中执行的,我们使用 posix 标准中的 pthread_self()函数打印线程 id,这段代码执行后的输出如下:

    Position 1, thread id: 4524555712
    Position 2, thread id: 4524555712
    Position 3, thread id: 123145337905152
    Position 4, thread id: 4524555712
    Result: 103
    

    我们可以看到,位置 1、2、4 三处的线程 id 打印结果相同,即 producer、以及取得计算线程执行结果的consume 函数都在外部线程执行,而位置 3 打印的线程 id 与其他三处都不同,也就是说 job 是在后台线程中执行。

    以上就是 Worker 的基本用法,但这其中有几个点需要注意,job 作为一个 lambda 表达式,不能随意捕捉上下文中的变量,进入 job 的参数必须从 producer 传入(producer 的返回值即为 job 的参数)。考虑一种情况,如果我们在主线程中得到了一个结果,然后想将它传递给 Worker,很自然的我们可能会写出如下代码:

    fun main() {
        val worker = Worker.start(true, "worker1")
        val testData = TestData()
        val future = worker.execute(TransferMode.SAFE, { testData }) {
            it
        }
        future.consume { println(it.index) }
    }
    
    
    
    
    data class TestData(var index: Int = 0)
    

    但这段代码会在运行时抛出 IncorrectDereferenceException 异常,因为 testData 虽然是用 val修饰的,但它不是 String 或原生类型,因此它不是一个被冻结的对象。仔细分析一下这段代码,在主线程中 testData对象初始化之后,紧接着会执行 producer 内的代码,当 producer 执行完毕后,异步的 job内的代码就会开始执行,但是主线程依然可以引用到 testData,这时就会发生并发访问的问题。那么如何避免这个问题?修改代码:

    fun main() {
        val worker = Worker.start(true, "worker1")
        var testData: TestData? = TestData()
        val future = worker.execute(TransferMode.SAFE, {
            val result = testData!!
            testData = null
            result
        }) {
            it
        }
        future.consume { println(it.index) }
    }
    
    
    
    
    data class TestData(var index: Int = 0)
    

    我们只需在 producer 返回前解除对需要传递的对象的引用,代码就可以正常运行,但上面这段代码只是一个为了便于理解的例子,在真正的软件开发当中,我们只需要将需要传递的值不向 producer 作用域之外暴露即可。

    现在我们回过头来看看 execute 的第一个参数,它代表对象转移校验模式,是一个枚举类型,共有 SAFE 与 UNSAFE两个值可选,在上面的示例中,我们都使用的是 SAFE 模式,现在我们把它更换为 UNSAFE 模式并编写一个典型的并发写程序:

    fun main() {
        val worker = Worker.start(true, "worker1")
        val testData = TestData()
        val future = worker.execute(TransferMode.UNSAFE, { testData }) { data ->
            repeat(20000) { data.index++ }
            data
        }
        repeat(20000) { testData.index++ }
        future.consume { println(it.index) }
    }
    
    
    
    
    data class TestData(var index: Int = 0)
    

    在 UNSAFE 模式下,testData 作为一个非冻结的对象也能任意传递到子线程中,如果这段代码中的线程调用是安全的,那么最终打印输出的结果应该是 40000,但很可惜,如果多次运行这段代码,每次它的打印输出结果都会不同,且小于

    40000。也就是说 UNSAFE 模式下,Worker 不做任何线程安全的校验(无论是编译期还是运行时)。

    这个结论与我预先猜测的不同,在源代码的注释中,对于 UNSAFE 是这样描述的:"Skip reachibility check, can lead to mysterious crashes in an application."。所以我预先猜测的是,如果没有发生事实上的多线程竞争,程序会正常运行,但是一旦发生多线程竞争,程序会抛出异常并崩溃。

    但测试结果却不是这样,一旦使用 UNSAFE 模式,代码就变得和在 Java 中编写不加任何同步机制的并发访问代码一样不安全,任何的潜在风险都不会被显式的表现出来,因此 UNSAFE 模式的注释中,官方也写了下面这句话:"USE UNSAFE MODE ONLY IF ABSOLUTELY SURE WHAT YOU'RE DOING!!!"。

    在这里我给出的建议是,如果能用语言机制规避的风险,就不要交给"人",因此,在 99.99% 的情况下,都应该尽量使用 SAFE模式,虽然 SAFE 模式对于对象的传递在语法上有更严格的限制,但是如果为了图方便使用 UNSAFE,在代码发生修改之后的潜在风险非常之大。

    2.3 对象子图

    这一小节主要讨论一个概念,即我们该怎样理解 Kotlin/Native 是如何检测一个对象是否在多个线程/Worker 中是可访问的?

    在官方文档中提到了对象子图(subgraph)的概念,详见参考链接 3。但是由于其资料较少,以下是我的个人理解:

    "在我们使用 Worker 的时候, Worker 会将 producer 返回的对象进行包装,生成一个对象子图(subgraph),我们可以将对象子图理解为一个对象,或是将它理解为一个对象头(因为这看起来有点类似在 TCP/IP报文头上添加 HTTP 报文头的感觉),它与原对象相互引用。每次在线程中访问对象的时候,都会通过 O(N) 复杂度的算法(官方未说明具体算法)来检测该对象是否在多个线程内可见。上面讨论的对象冻结,也是通过对象子图来实现的。"

    对象子图在某些特殊的情况下可以与对象分离,从而让我们可以自由的让对象在多个线程间访问,这虽然不安全,但也是如果我们要使用其它同步机制(例如一些平台相关的同步机制或协程的 Mutex)必须要进行的步骤,有关对象子图分离的内容将在3.3 小节与协程的 Mutex 一起详细介绍。

    2.4 单例与全局变量

    对于单例与全局变量来说(成员变量也类似),在 Worker 中对其进行直接的访问是无法避免的,我们不能每次都通过 producer将单例或全局变量传递给 Worker 之后就将其置空,因此在 Kotlin/Native 中,单例与全局变量有着特别的规则。

    先来介绍一下 @ThreadLocal 注解,编写一个示例:

    @ThreadLocal
    val testData = TestData()
    
    
    
    
    fun main() {
        val worker = Worker.start(true, "worker1")
        val future = worker.execute(TransferMode.UNSAFE, {}) {
            println(++testData.index)
        }
        future.consume { println(testData.index) }
    }
    
    
    
    
    data class TestData(var index: Int = 0)
    

    运行这段代码的输出如下:

    1
    0
    

    被添加了 @ThreadLocal 注解的全局变量会在每个线程中维护一个单独的副本,即在线程中对其进行修改对于其他线程是不可见的。在上面这个例子中,我们在 Worker 内对 testData.index 进行了自增操作,然而在主线程中则感知不到它的变化。

    我们在讨论对象冻结的时候提到过 @SharedImmutable 注解,现在我们使用 @SharedImmutable 替换 @ThreadLocal然后运行程序,程序崩溃并抛出 InvalidMutabilityException 异常,如果我们再将 ++testData.index这一行中的 ++ 去掉,程序正常运行,这说明,对于开发者"手动"冻结的对象,并发的读取不会有问题,但是一旦其中一个线程/Worker要对变量进行修改,就会抛出 InvalidMutabilityException 异常。

    对于单例(使用 object 关键字声明的),在不加任何特别注解的情况下,它都是冻结的,你可以认为它是一个默认添加了 @SharedImmutable注解的全局变量,但如果有特别的需要,也可以给单例添加 @ThreadLocal 注解,让它变成一个线程局部的可变变量,关于单例的代码示例不再给出。

    三、预览版的多线程协程

    在上面的章节中,我们介绍的 Worker 与对象子图是在 Kotlin/Native 在诞生之初就已经定型的异步并发模型,而 Kotlin/Native上的协程长久以来都只支持单线程,这就使得 Native 版的协程相对于 JVM 版功能大打折扣,但好消息是,近期在协程的官方 Github仓库(kotlinx.coroutines)的 issue#462(参考链接 5)中,Kotlin 官方团队的 Roman Elizarov 提到了已经发布了第一个多线程协程的预览版本,这也让 Kotlin/Native的开发者们看到了官方支持多线程协程的决心。

    但需要说明的是,当前多线程版本的协程仅仅是一个早期预览版,从目前的体验情况来看,后续的改动一定会不小,因此本文仅仅是做一个尝试,Native 上的多线程协程的最终形态还要等正式版推出之后才能确定。

    若要导入当前主分支版本的协程,可以添加如下依赖:

    dependencies {
        implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core-native:1.3.3"
    }
    

    如果您想尝鲜预览版的多线程协程,则可以添加如下依赖:

    dependencies {
        implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core-native:1.3.3-native-mt"
    }
    

    3.1 Default 与 Main 调度器的指向发生破坏性变更

    在主分支的协程中,Dispatchers 下的两个调度器 Dispatchers.Main 与 Dispatchers.Default指向同一个线程,即主线程(程序最初初始化的线程)。而在多线程版的协程中 Dispatchers.Default变更为指向一个后台单线程,我们通过如下代码示例即可验证:

    fun main() {
        println("Position 1, thread id: ${pthread_self()!!.rawValue.toLong()}")
        GlobalScope.launch(Dispatchers.Default) {
            println("Position 2, thread id: ${pthread_self()!!.rawValue.toLong()}")
        }
        GlobalScope.launch(Dispatchers.Main) {
            println("Position 3, thread id: ${pthread_self()!!.rawValue.toLong()}")
        }
        CFRunLoopRun() // Create Darwin main thread loop
    }
    

    注意,Dispatchers.Default 是单线程而不是多线程组成的线程池的说法详见参考链接 4,可自行验证。

    输出打印如下:

    Position 1, thread id: 4664880576
    Position 2, thread id: 123145451188224
    Position 3, thread id: 4664880576
    

    如打印结果所示,位置 1 与 3 的线程 id 相同,而位置 2 则与前面两者不同,这说明了经 Dispatchers.Default调度的协程运行在一个后台线程中。在这里 main 函数体与经 Dispatchers.Main调度后的协程都运行在主线程内。不过这里有一点需要注意 Dispatchers.Main调度器在所有 Darwin(即全部 Apple 平台:iOS、macOS、watchOS、tvOS 等等)上调度方式改用了平台相关的RunLoop,在上面的示例中,我们使用 CFRunLoopRun 函数开启了主线程循环,所以 Dispatchers.Main调度器才会有效,如果我们使用协程的 runBlocking 函数开启主线程循环,则 Dispatchers.Main调度器在 Darwin 平台上将失效。考虑以下代码示例:

    fun main() = runBlocking {
        launch(Dispatchers.Main) { 
            println("Run on the main thread")
        }
        Unit
    }
    

    上面这段代码在主分支的协程中所有的 Native 平台上都可以正常打印,但在多线程版协程中,如果目标平台为Darwin,则协程内部的打印输出将永远不会生效,但在 Linux、Windows 等平台上仍可以正常打印。这实际上是一个进步,如果我们要编写移动端的多平台程序,我们会更希望 Dispatchers.Main 在 iOS 上切换到 UI 主线程。

    3.2 利用 CPU 多核能力的主要方式:newSingleThreadContext() 函数

    Dispatchers.Default 调度器虽然可以将您当前在协程中执行的异步代码切换到后台线程,但它与 Kotlin/JVM上的 Dispatchers.Default 线程池实现相比,仍然力有不足。如果您想充分利用 CPU 的多核性能,Native 的 Dispatchers.Default

    仍然不能满足您的需求。但是当前预览版本的多线程协程中仍然没有线程池的实现,因此我们必须手动创建其他的多线程上下文。

    在主分支版本的协程上,程序无法引用到 newSingleThreadContext() 函数,它曾经是 Kotlin/JVM独有的,但当前 Kotlin/Native 的预览版的多线程协程中,newSingleThreadContext() 是我们使用 CPU 多核能力的主力调度器,见如下代码示例:

    @UseExperimental(ExperimentalCoroutinesApi::class)
    fun main() = runBlocking {
        println(pthread_self()!!.rawValue.toLong())
        launch(newSingleThreadContext("1")) {
            println(pthread_self()!!.rawValue.toLong())
        }
        launch(newSingleThreadContext("2")) {
            println(pthread_self()!!.rawValue.toLong())
        }
        Unit
    }
    

    输出打印如下:

    4703317440
    123145445687296
    123145446223872
    

    每一个 newSingleThreadContext() 都会创建一个新的线程,所以真正正确的用法是我们每次都应该把 newSingleThreadContext()创建的 CoroutineContext 保存起来然后重复使用,当我们不再需要一个由 newSingleThreadContext()产生的  CoroutineContext 时,我们应该手动将其回收以释放资源,如下所示:

    @UseExperimental(ExperimentalCoroutinesApi::class)
    fun main() = runBlocking {
        println(pthread_self()!!.rawValue.toLong())
        val coroutineContext = newSingleThreadContext("1")
        val job = launch(coroutineContext) {
            println(pthread_self()!!.rawValue.toLong())
        }
        job.join()
        coroutineContext.close()
    }
    

    此外,由于 Kotlin/Native 中积极推行 Worker 取代线程的概念,因此通过 newSingleThreadContext()产生的 CoroutineContext 可以直接通过成员属性 worker 引用到该线程对应的 Worker,如下所示:

    @UseExperimental(ExperimentalCoroutinesApi::class)
    fun main() = runBlocking {
        println(pthread_self()!!.rawValue.toLong())
        val coroutineContext = newSingleThreadContext("1")
        val job = launch(coroutineContext) {
            println(pthread_self()!!.rawValue.toLong())
        }
        job.join()
        coroutineContext.worker.execute(TransferMode.SAFE, {}) {
            "Hello Multi-thread"
        }.consume { 
            println(it)
        }
        coroutineContext.close()
    }
    

    3.3 对象子图分离与失效的 Mutex

    协程构建器(例如 launch、async 等)的参数 lambda 表达式可以任意捕捉上下文变量,它将默认捕捉的变量都是冻结的(这里指的是局部变量),即,如果协程所运行的线程与外部线程不同,且如果发生修改这些捕捉过来的变量时,则程序都会抛出 InvalidMutabilityException 异常。

    但是在协程中,我们有协程自己的基于挂起实现的锁 Mutex,因此如果要使用 Mutex 来保证并发安全,第一步要做的就是让变量的更改摆脱Worker-对象子图机制,完全将并发风险暴露出来,然后才能通过将有风险的代码包裹在 Mutex 锁的作用域内来充分利用 Mutex。

    然而,在协程构建器与 Worker 的 execute 函数不同,不能将协程本身设置为 UNSAFE 模式,因此这里需要将对象子图暂时分离,然后在协程构建器内再将其重新绑定。用法如下面的代码示例所示:

    fun main() = runBlocking {
        val testData = TestData()
        val bareTestData = DetachedObjectGraph(TransferMode.UNSAFE) { testData }
        val job = launch(Dispatchers.Default) {
            val outTestData = bareTestData.attach()
            repeat(20000) { outTestData.index++ }
        }
        repeat(20000) { testData.index++ }
        job.join()
        println(testData.index)
    }
    
    
    
    
    data class TestData(var index: Int = 0)
    

    为了便于理解代码,我们可以用下图更直观的解释对象子图,以及对象子图分离的过程:

    虽说叫做对象子图分离,但是在用法上却更类似于包装,我们使用 DetachedObjectGraph<T>类来包装一个对象,即可实现对象子图分离。DetachedObjectGraph<T> 的构造函数接收两个参数,第一个是对象转移校验模式TransferMode,可以看到,如果要达成我们的目的,这里必须使用 UNSAFE 模式,第二个参数则类似于 execute函数的 producer。然后我们在需要使用它的协程中再调用 DetachedObjectGraph<T> 类的扩展函数attach,即可以拿到原对象。DetachedObjectGraph<T> 类的另一个构造函数重载接收一个 COpaquePointer?类型的参数(代表一个指针),感兴趣的读者可以自行尝试。

    这段代码的运行后的打印输出结果与上文展示的 execute 函数的 UNSAFE 模式如出一辙,最终输出的值一定小于 40000(如果并发安全的话会输出 40000 整)。

    然后,我们将上面的代码添加到协程的并发安全机制 Mutex 中来,示例代码如下所示:

    fun main() = runBlocking {
        val testData = TestData()
        val bareTestData = DetachedObjectGraph(TransferMode.UNSAFE) { testData }
        val mutex = Mutex()
        val job = launch(Dispatchers.Default) {
            val outTestData = bareTestData.attach()
            repeat(20000) {
                mutex.withLock { outTestData.index++ }
            }
        }
        repeat(20000) {
            mutex.withLock { testData.index++ }
        }
        job.join()
        println(testData.index)
    }
    

    很可惜,当前预览版的多线程协程的 Mutex 存在 bug,一旦两个协程发生事实上的 Mutex 锁竞争,Mutex 就会将协程一直挂起而不恢复,这会导致我们永远看不到输出结果,如果将上面的代码剔除掉与 Native 有关的部分(例如对象子图分离),然后拿到 Kotlin/JVM上运行,可以正常得到输出:"40000",剔除与 Native 相关部分的代码如下所示:

    fun main() = runBlocking {
        val testData = TestData()
        val mutex = Mutex()
        val job = launch(Dispatchers.Default) {
            repeat(20000) {
                mutex.withLock { testData.index++ }
            }
        }
        repeat(20000) {
            mutex.withLock { testData.index++ }
        }
        job.join()
        println(testData.index)
    }
    

    这说明 Mutex 的功能在后续有待修复。

    除了 Mutex 外,官方还有另一种建议使用的实现并发安全的机制——基于 actor 协程构建器与 Channel的消息机制。但该机制由于目前 actor 协程构建器在 Kotlin/Native 上不可用也暂时无济于事。

    四、总结

    在本文中我们一共体验了两套 Kotlin/Native 中实现异步与并发的方式,Worker-对象子图模式虽然可以确保并发安全,但是其做法较为粗暴,但目前来说 Worker-对象子图模型仍然是较为成熟的一套实现异步与并发的机制。

    多线程版的协程由于处在预览版,因此问题也非常的多,目前已知的问题包括:

    1)Dispatchers.Default 调度器功能有限,与 Kotlin/JVM 版的差距太大,但官方资料(参考链接 4)提到后续 Dispatchers.Default 有可能会变更为多线程版本。

    2)基于协程挂起实现的锁 Mutex 存在 Bug,当前会造成协程的长时间挂起且不恢复。

    3)官方资料(参考链接 4)中提到,当前预览版的多线程协程存在内存泄漏。

    4)由于 Dispatchers.Default 与 Dispatchers.Main 调度器指向的线程发生了破坏性变更,如果您之前已经在工程中使用了主分支的单线程版线程,可能会面临代码迁移的问题。

    当然,协程与已存在的 Worker-对象子图模型之间也并不协调,就如同上面的示例,如果要使用协程的并发安全机制保证并发安全,就必须进行对象子图分离。然而对象子图的概念在 Kotlin/JVM 上并不存在,这会导致使用协程编写的代码不能做到真正的平台无关。

    从长远来看,协程-挂起机制是 Kotlin 的核心,如果后续 kotlinx.io库完整实现了基于 suspend 的 I/O,那么协程就可以一统Kotlin 上的所有异步并发场景,因此,Worker-对象子图模型与多线程的协程之间会如何调和的更优雅,还有待官方后续的完善。

    当前,Kotlin/Native 已经经过了接近三年左右的实验性阶段,进入了一个"相对稳定"的状态,据说 2020 年发布的 Kotlin 1.4会让 Kotlin/Native 进入正式版,如果想要试验 Kotlin/Native在线上产品中是否可行,个人认为,只要经过大量且完备的测试(虽然做起来并不容易),以目前状况来看,是值得一试的,但预览版的多线程协程则不同,它处在一个非常非常早期的预览阶段,想要在线上产品中使用,还要等待官方后续推出更加稳定的版本。

    参考文档

    参考链接 1:Kotlin 编译器实现协程的主要工作是 CPS 变换与状态机,官方 KEEP:

    https://github.com/Kotlin/KEEP/blob/master/proposals/coroutines.md

    参考链接 2:Java 计划在 JDK 15 中添加类似协程的异步并发工具,即 Project Loom:https://wiki.openjdk.java.net/display/loom/Main#Main-Design

    参考链接 3:Kotlin/Native 关于异步并发模型的官方文档:https://kotlinlang.org/docs/reference/native/concurrency.html

    参考链接 4:Roman Elizarov 编写的关于多线程版 Native 协程的官方资料:https://github.com/Kotlin/kotlinx.coroutines/blob/native-mt/kotlin-native-sharing.md

    参考链接 5:关于 Native 多线程协程的 issue:issue#462 https://github.com/Kotlin/kotlinx.coroutines/issues/462

    【推荐阅读】

     “携程技术”公众号

      分享,交流,成长

    展开全文
  • Kotlin Native实战开发

    千次阅读 2018-11-18 09:15:36
    Kotlin Native是一种将Kotlin源码编译成不需要任何VM支持的目标平台二进制数据的技术,编译后的二进制数据可以直接运行在目标平台上,它主要包含一个基于LLVM的后端编译器的和一个Kotlin本地运行时库。设计Kotlin ...

    注:本部分内容来源于《Kotlin入门与实战》,预计9月上市。

    16.1 Kotlin Native

    16.1.1 Kotlin Native简介

    Kotlin Native是一种将Kotlin源码编译成不需要任何VM支持的目标平台二进制数据的技术,编译后的二进制数据可以直接运行在目标平台上,它主要包含一个基于LLVM的后端编译器的和一个Kotlin本地运行时库。设计Kotlin Native的目的是为了支持在非JVM环境下进行编程,如在嵌入式平台和iOS环境下,如此一来,Kotlin就可以运行在非JVM平台环境下。

    LLVM是Low Level Virtual Machine的缩写,是一种比较底层的虚拟机技术,LLVM由C++编写而成,主要用来优化应用程序的编译时间(compile-time)、链接时间(link-time)、运行时间(run-time)以及空闲时间(idle-time)。LLVM可以有效的解决编译器重复编译代码的问题,并且LLVM制定了LLVM IR这种中间代码表示语言,LLVM IR充分考虑了各种应用场景,有效的提高了代码的编译效率。

    在讲解Kotlin Native具体的知识之前,先来看一下计算机高级语言常见两种流派:编译型语言和解释型语言。

    所谓编译型语言,是指使用专门的编译器、针对特定平台/操作系统将某种高级语言源代码一次性编译成该平台硬件能够执行的机器码,编译生成的可执行程序可以脱离开发环境,在特定的平台上独立运行。因为编译型语言是一次性编译成机器码的,所以可以脱离开发环境独立运行,而且通常运行效率较高。不过,正因为编译型语言只能被编译成特定平台上的机器码,所以生成的可执行程序通常无法移植到其他平台上运行。例如,现在比较流行的C、C++等高级编程语言都属于编译型语言。

    而所谓解释型语言,是指使用专门的解释器对源程序进行逐行解释,并生成特定平台的机器码并立即执行的编程语言。解释型语言通常不需要进行整体的编译和链接处理,解释型语言会把编译型语言中的编译和解释过程混合在一起执行。虽然解释型语言运行效率较低且不能脱离释器独立运行,但解释型语言可以很方便的实现源程序的移植和运行。

    ##16.1.2 Kotlin Native编译器
    目前,Kotlin Native主要提供了Mac、Linux和Windows三个主流平台的编译器,使用该编译器可以很轻松的编译出运行在树莓派、iOS、OS X、Windows以及Linux系统上的程序。Kotlin Native支持平台和版本如下表所示。

    支持的系统平台	支持的版本
    Windows	x86_64
    Linux	x86_64、arm32、MIPS、MIPS小端
    MacOS	x86_64
    iOS	arm64
    Android	arm32、arm64
    WebAssembly	wasm32
    

    表16-1 Kotlin Native支持平台及其版本

    编译Kotlin Native项目,首先需要到Github上下载Kotlin Native的编译器软件包,下载地址为:https://github.com/JetBrains/kotlin-native/releases,使用前下载对应的平台版本即可,下载后解压下载的Kotlin Native编译器软件包,其目录结构如图16-1所示。
    这里写图片描述
    图16-1 Kotlin Native编译器目录结构图
    当然,也可以通过克隆Kotlin Native编译器的源码进行编译,编译需要先到Github上下载编译器源码,下载地址为:https://github.com/JetBrains/kotlin-native。下载完成后,使用如下命令下载依赖关系,命令如下:

    ./gradlew dependencies:update
    

    然后,建立编译器和库的关联。

    ./gradlew bundle
    

    如果需要构建整个项目可能需要很长的时间。然后,使用以下的命令即可编译项目。

    ./gradlew dist distPlatformLibs
    

    到此,就可以得到Kotlin的Native编译器了,它通常位于项目的./dist/bin目录下,打开bin文件可以看到Native编译器的相关信息,它有7个可执行程序构成,如图15-2所示。通过对比发现,Native编译器的目录结构和Kotlin Native官方提供的编译器的内容是一样的。然后,就可以利用Native编译器编译应用程序了。例如:

    export PATH=./dist/bin:$PATH
    kotlinc hello.kt -o hello
    

    如果需要进行优化编译,可以使用-opt参数。

    kotlinc hello.kt -o hello -opt
    

    如果需要对应用程序进行测试,可以使用类似于下面的命令。

    ./gradlew backend.native:tests:run
    

    这里写图片描述

    图16-2 Kotlin的Native编译器目录结构
    在Kotlin Native官方提供的示例中,系统自带了针对不同平台的例子,并且这些例子都是可以直接编译运行的。由于Kotlin Native本身是一个gradle构建的项目,所以可以使用idea直接打开Kotlin Native目录下的samples文件,idea会自动识别该项目。
    ##16.1.3 编译器konan
    打开kotlin-native-macos-0.6文件,其目录结构如图15-3所示。其中,bin目录包含众多的与Kotlin Native相关的执行命令,klib目录则主要包含Kotlin的标准库的关联元数据文件以及针对各个目标平台的bc文件,konan主要包含编译器依赖的一些jar包和一些已经编译好的项目实例,可以使用IntelliJ IDEA直接导入。
    这里写图片描述
    图16-3 编译器konan目录结构
    打开Kotlin Native编译器的bin目录可以发现,bin文件主要由cinterop、jsinterop、klib、konanc、kotlinc、kotlinc-native、run_konan等7个可执行文件组成。其中,run_konan是编译器真正的入口,源码如下。

    TOOL_NAME="$1"
    shift
    
    if [ -z "$JAVACMD" -a -n "$JAVA_HOME" -a -x "$JAVA_HOME/bin/java" ]; then
        JAVACMD="$JAVA_HOME/bin/java"
    else
        JAVACMD=java
    fi
    [ -n "$JAVACMD" ] || JAVACMD=java
    //省略部分代码
    
    LIBCLANG_DISABLE_CRASH_RECOVERY=1 \
    $TIMECMD "$JAVACMD" "${java_opts[@]}" "${java_args[@]}" -cp "$KONAN_CLASSPATH" "$TOOL_CLASS" "$TOOL_NAME" "${konan_args[@]}"
    

    可以发现,Kotlin Native编译器konan的运行环境还是需要JVM环境支持的,但是它生成的机器码的可执行程序是不需要JVM环境支持的,可以直接运行在对应的平台系统上。

    16.2 Kotlin Native实例

    ##16.2.1 构建Kotlin Native项目
    首先,在IDEA中依次选择【File】→【New】→【Project】创建一个普通的 Gradle工程。
    这里写图片描述
    图16-4 创建Gradle工程
    ##16.2.2 添加konan插件配置
    创建完成之后,需要修改build.gradle文件配置。打开build.gradle文件并添加如下配置。

    buildscript {
        repositories {
            mavenCentral()
            maven {
                url "https://dl.bintray.com/jetbrains/kotlin-native-dependencies"
            }
        }
        dependencies {
            classpath "org.jetbrains.kotlin:kotlin-native-gradle-plugin:0.5"
        }
    }
    apply plugin: 'konan'
    

    其中,kotlin-native-gradle-plugin:0.5是Gradle构建Kotlin Native工程所使用的DSL插件,这个插件发布在https://dl.bintray.com/jetbrains/kotlin-native-dependencies仓库里。除此之外,还需要应用konan插件,konan插件是用来将Kotlin代码编译为native代码的插件,可以通过如下地址来获取更多konan相关的信息。

    https://github.com/JetBrains/kotlin-native/blob/master/GRADLE_PLUGIN.md
    

    此时,还需要创建一个kotliner.def文件,该文件主要用来配置C源码到Kotlin的映射关系。

    headers=cn_kotliner.h
    

    ##16.2.3 编写源代码
    接下来,在工程的src目录下新建一个c目录,此目录专门用来存放C代码。首先,创建两个c文件:cn_kotliner.h和cn_kotliner.c。其中,C头文件声明的代码如下。

    #ifndef CN_KOTLINER_H
    #define CN_KOTLINER_H
    
    void printHello();
    int factorial(int n);
    #endif
    

    在上面的代码中,主要声明了两个函数,打印HelloWorld的 printHello函数和用来计算阶乘的factorial函数。cn_kotlinor.c的源代码如下:

    #include "cn_kotliner.h"
    #include <stdio.h>
    
    void printHello(){
        printf("[C]HelloWorld\n");
    }
    
    int factorial(int n){
        printf("[C]calc factorial: %d\n", n);
        if(n == 0) return 1;
        return n * factorial(n - 1);
    }
    

    接下来,还需要创建一个kotlin文件,该文件主要是调用C层的代码,实现跨平台调用。该文件的源码如下:

    import kotliner.*
    
    fun main(args: Array<String>) {
        printHello()
        (1..5).map(::factorial).forEach(::println)
    }
    

    其中,导入的kotlinor.*包是C语言代码经过clang编译后对应的C接口的包路径,可以在项目的build.gradle配置文件中的konanInterop中配置这个路径。
    这里写图片描述
    图16-5 Kotlin Native项目目录结构图

    ##16.2.4 添加konanInterop与konanArtifacts配置
    接下来,还需要添加konanInterop和konanArtifacts相关的配置信息。其中,konanInterop主要用来配置Kotlin调用C的接口。相关的源码如下:

    konanInterop {
        ckotlinor {
            defFile 'kotlinor.def'     // interop配置文件
            includeDirs "src/c"      // C头文件目录,可以传入多个
        }
    }
    

    在上面的配置文件中,ckotlinor是插件中的KonanInteropConfig对象,在konanArtifacts配置中会引用这个ckotlinor。而kotlinor.def是Kotlin Native与C 语言互操作的配置文件,可以在kotlinor.def里面配置C源码到Kotlin的映射关系,该配置文件的内容如下。

    headers=cn_kotlinor.h
    compilerOpts=-Isrc/c
    

    除此上面使用的选项之外,konanInterop还提供了如下常用的选项。

    konanInterop {
           pkgName {
               defFile <def-file>  
               pkg <package with stubs>
               target <target: linux/macbook/iphone/iphone_sim>
               compilerOpts <Options for native stubs compilation>
               linkerOpts <Options for native stubs >
               headers <headers to process> 
               includeDirs <directories where headers are located> 
               linkFiles <files which will be linked with native stubs>
               dumpParameters <Option to print parameters of task before execution>
           }   
     }
    

    konanInterop配置参数选项对应的具体含义如下表所示。

    配置选项	选项说明
    defFile	互操作映射关系配置文件
    pkg	C头文件编译后映射为Kotlin的包名
    target	编译目标平台:linux/macbook/iphone等
    compilerOpts	编译选项
    linkerOpts	链接选项
    headers	需要处理的头文件
    includeDirs	包括头文件的目录
    linkFiles	与native stubs链接的文件
    dumpParameters	打印Gradle任务参数的选项配置
    

    表16-2 konanInterop配置选项说明表
    接下来,需要为项目添加konanArtifacts相关的配置,该配置主要用来处理编译任务的执行。

    konanArtifacts { 
        KotlinorClient {   
            inputFiles fileTree("src/kotlin")  //kotlin代码配置,项目入口main()
            useInterop 'ckotlinor'   //前面的interop配置
            nativeLibrary fileTree('src/c/cn_kotlinor.bc')   //本地库文件配置
            target 'macbook'   // 编译的目标平台
        }
    } 
    

    konan编译任务配置的处理类是KonanCompileTask.kt,可以在Kotlin Native的kotlin-native-gradle-plugin插件中找到该类。可以通过以下地址来获取更详细的konan插件配置信息。

    https://github.com/JetBrains/kotlin-native/blob/master/GRADLE_PLUGIN.md
    

    16.2.5 编译与执行

    接下来,在项目的src/c目录下面,用命令行编译上面的代码,命令如下。

    clang -std=c99 -c cn_kotliner.c -o cn_kotliner.bc -emit-llvm
    

    其中,clang是一个由C++编写的基于LLVM的C/C++/Objective-C/Objective-C++编译器。如果提示找不到clang命令,可以在编译器的dependencies目录中找到相关的内容。当然,还可以使用shell脚本(名称为kclang.sh)来简化clang编译的命令行输入参数。

    #!/usr/bin/env bash clang -std=c99 -c $1 -o $2 -emit-llvm
    

    接着把kclang.sh放到C代码目录下,然后使用脚本来编译C代码。例如:

    kclang.sh cn_kotlinor.c cn_kotlinor.bc
    

    通过上面的命令编译之后,将得到一个名为cn_kotlinor.bc的库文件。最后,在执行Gradle构建之前,还需要指定konan编译器主目录。具体的,在工程根目录下面新建一个gradle.properties属性配置文件,该文件格式如下。

    konan.home=<编译器路径>
    

    例如:

    konan.home=/Users/xiangzhihong /kotlin native/kotlin-native-macos-0.5
    

    当然,也可以不添加gradle.properties配置文件,那样的话,只需要在编译的时候使用本地的编译器即可。
    然后,在IDEA的Gradle工具栏依次点击【Tasks】→【build】执行构建操作,如图15-5所示。等待项目构建完成,会在项目的build/konan/bin/目录下面生成一个KotlinorClient.kexe的可执行程序,它可以直接运行在Mac OS系统上而不再需要依赖JVM环境。
    这里写图片描述
    图16-5 使用Gradle工具栏编译项目
    然后,在命令行中执行KotlinorApp.kexe命令,即可看到输出结果,对应的命令如下。
    build/konan/bin/KotlinorApp.kexe
    可以看到,作为一款致力于跨平台开发的编程语言,Kotlin Native非常注重语言平台的互操作性,可以说,使用Kotlin Native进行跨平台开发优势是非常明显的。

    16.2.6 命令行方式编译Kotlin Native

    对于Kotlin Native项目来说,除了允许Gradle方式构建编译外,还可以使用命令行的方式来编译项目。具体来说,编写完Kotlin源码之后,采用shell脚本的方式来构建,或者使用Makefile或build.sh的方式来构建,官方推荐使用shell脚本构建方式,本篇采用与之类似的Makefile脚本方式。例如:

    build : src/kotlin/main.kt kotliner.kt.bc
        konanc src/kotlin/main.kt -library build/kotliner/kotliner.kt.bc -nativelibrary build/kotliner/cn_kotliner.bc -o build/kotliner/kotliner.kexe
    
    kotliner.kt.bc : kotliner.bc kotliner.def
        cinterop -def ./kotliner.def -o build/kotliner/kotliner.kt.bc
    
    kotliner.bc : src/c/cn_kotliner.c src/c/cn_kotliner.h
        mkdir -p build/kotliner
        clang -std=c99  -c src/c/cn_kotliner.c -o build/kotliner/cn_kotliner.bc -emit-llvm
    
    clean:
          rm -rf build/kotliner
    

    采用命令行方式编译Kotlin Native时,需要先把编译器<konan.home>/bin目录加入系统的path环境中,然后再执行make命令,编译完成之后就可以在项目的build/kotliner目录中找到kotliner.kexe文件。

    在这里插入图片描述

    展开全文
  • kotlin native 再次尝试

    千次阅读 2019-02-12 12:44:08
    使用IDEA创建了一个kn工程 gradle配置文件长这样 ... id 'kotlin-multiplatform' version '1.3.21' } repositories { mavenCentral() } kotlin { targets { // For ARM, preset should be changed to pre...

    之前的一篇:IDEA运行kotlin native

    使用IDEA创建了一个kn工程
    在这里插入图片描述
    gradle配置文件长这样

    plugins {
        id 'kotlin-multiplatform' version '1.3.21'
    }
    repositories {
        mavenCentral()
    }
    kotlin {
        targets {
            // For ARM, preset should be changed to presets.iosArm32 or presets.iosArm64
            // For Linux, preset should be changed to e.g. presets.linuxX64
            // For MacOS, preset should be changed to e.g. presets.macosX64
            fromPreset(presets.linuxX64, 'linux')
    
            configure([linux]) {
                // Comment to generate Kotlin/Native library (KLIB) instead of executable file:
                compilations.main.outputKinds('EXECUTABLE')
                // Change to specify fully qualified name of your application's entry point:
                compilations.main.entryPoint = 'sample.main'
            }
        }
        sourceSets {
            // Note: To enable common source sets please comment out 'kotlin.import.noCommonSourceSets' property
            // in gradle.properties file and re-import your project in IDE.
            linuxMain {
            }
            linuxTest {
            }
        }
    }
    
    task runProgram {
        def buildType = 'release' // Change to 'debug' to run application with debug symbols.
        dependsOn "link${buildType.capitalize()}ExecutableLinux"
        doLast {
            def programFile = kotlin.targets.linux.compilations.main.getBinary('EXECUTABLE', buildType)
            exec {
                executable programFile
                args ''
            }
        }
    }
    

    可使用gradle运行
    在这里插入图片描述
    这一次比我之前试多了一点代码提示,然后编译依旧超级慢,而且无法调试
    然后用CLion试了下
    在这里插入图片描述
    这个main文件夹很奇特
    在这里插入图片描述
    gradle配置还是像纯文本一样
    在这里插入图片描述
    运行依赖gradle,跟IDEA没啥区别
    编译慢,无法调试
    当前gradle版本5.0
    当前kotlin版本1.3.21
    总的来说,kn没啥大的变化

    群里说clion可以调试,我还纳闷
    重新试了下,还真可以调试
    在这里插入图片描述

    展开全文
  • Kotlin Native编程探索

    千次阅读 2018-04-14 13:02:02
    Kotlin简介 Kotlin是JetBains开发一种基于JVM的新的编程语言。Kotlin可以编译成字节码运行在JVM上,与Java完美兼容,并在...Kotlin Native利用LLVM来将Kotlin代码编译成本地机器代码,使得Kotlin可以脱离JVM运行。...

    Kotlin简介

    Kotlin是JetBains开发一种基于JVM的新的编程语言。Kotlin可以编译成字节码运行在JVM上,与Java完美兼容,并在Java的基础上添加了很多好用的特性。也正因为kotlin的种种优点,Google将Kotlin选为Android开发的一级语言。

    Kotlin Native利用LLVM来将Kotlin代码编译成本地机器代码,使得Kotlin可以脱离JVM运行。借助Kotlin Native,Kotlin也能被打包成lib、exe、dll等格式、运用于嵌入式或其他对性能要求较高的场景。

    Kotlin编译器准备

    这个编译器和Kotlin/JVM工程里的不一样,Kotlin/Native工程里是直接把代码编译成机器码而不是运行在虚拟机上的字节码。下载对应的编译器,或者直接从github上下载源码编译。直接下载release目录下的编译器可以略过下面的编译过程。

    首先运行下面的命令下载依赖包。

    ./gradlew dependencies:update
    

    然后运行下面的命令build编译器,因为编译整包的时间较长,所以建议使用下面的第二种方式或直接下载编译器。

    ./gradlew bundle     // 1.编译整包
    ./gradlew dist       // 2.只编译当前系统对应的包

    将Kotlin编译成机器码

    新建一个kotlin工程,选择使用gradle来构建。
    这里写图片描述
    在项目根目录下新建一个gradle.properties文件,配置编译器的路径。其中Windows下要注意路径分割符,Linux和mac因为分隔符是”/”所以正常写路径就行了。

    konan.home=D\:\\KotlinNative\\kotlin-native-windows-0.6.2         // 1.windows直接下载编译器
    konan.home=D\:\\KotlinNative\\kotlin-native\\dist                 // 2.windows下载源码编译

    在根目录下新建src/main/kotlin目录用于存放Kotlin代码,然后随手就是一个hello world。

    // Main.kt
    fun main(args: Array<String>) {
        println("Hello Kotlin Native")
    }

    编辑build.gradle的内容(其中我注释掉的内容为Kotlin/JVM的配置,可以让Kotlin代码在Intellij上运行,调试Kotlin代码的时候可以用),在把Kotlin编译成机器码的过程中只需要有Kotlin/Native的相关配置就行了。

    buildscript {
    //    ext.kotlin_version = '1.1.4'
    
        repositories {
            mavenCentral()
            maven {
                url "https://dl.bintray.com/jetbrains/kotlin-native-dependencies"
            }
        }
        dependencies {
            classpath "org.jetbrains.kotlin:kotlin-native-gradle-plugin:0.6"
    //        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        }
    }
    
    apply plugin: 'konan'
    //apply plugin: 'kotlin'
    
    repositories {
        mavenCentral()
    }
    
    //dependencies {
    //    compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    //}
    
    konanArtifacts {
        // 这里只是把src/main/kotlin/Main.kt编译成一个名为foo的可执行文件
        program('foo') {
            srcFiles 'src/main/kotlin/Main.kt'
        }
    }

    上面build.gradle中的配置,konanArtifacts这个括号里的内容就是控制打包的相关配置,是我们主要关注的,主要有以下配置项,可以根据自己的需要选择(个人翻译,可以去github看英文原版)。

    // 默认编译的平台
    konan.targets = ['macbook', 'linux', 'wasm32']
    
    // kotlin和api的版本
    konan.languageVersion = 'version'
    konan.apiVersion = 'version'
    
    konanArtifacts {
        // 编译的目标平台 (如果没有的话就是上面konan.targets里面的平台)
        // 生成foo.exe(windows)/foo.kexe(linux)
        program('foo', targets: ['android_arm32', 'android_arm64']) {
    
            // 源代码路径,默认路径为 src/main/kotlin.
            srcDir 'src/other'
    
            // 源文件.
            srcFiles project.fileTree('src')
            srcFiles 'foo.kt', 'bar.kt'
    
            // 生成文件的名字,如果没有就是上面小括号中的foo.
            artifactName 'customName'
    
            // 文件输出的路径,默认为 build/konan/bin
            baseDir 'path/to/output/dir'
    
            libraries {
                // 库文件
                file 'foo.klib'
                files 'file1.klib', file2.klib
                files project.files('file3.klib', 'file4.klib')
    
                // 当前工程中生成的其他文件
                artifact konanArtifacts.bar
                artifact 'baz'
    
                // 其他工程中的生成文件
                artifact project(':path:to:a:project'), 'artifcatName'
    
                // 其他某个工程中生成的所有库文件
                allLibrariesFrom project(':some:project')
    
                // 其他某个工程中的所有的互操作类型的库文件
                allInteropLibrariesFrom project(':some:interop:project')
    
                // 根据名字查找的.klib库文件
                klib 'foo'
                klib 'bar', 'baz'
            }
    
            // 需要链接的native库 (*.bc).
            nativeLibrary project.file('path/to/native/library.bc')
            nativeLibraries 'library1.bc', 'library2.bc'
    
            noStdLib true             // 不链接stdlib (true/false).
            enableOptimizations true  // 开启编译优化 (true/false).
            enableAssertions true     // 在生成二进制文件的时候开启断言 (true/false).
            enableDebug true          // 在生成二进制文件的时候开启debug (true/false).
            noDefaultLibs true        // 不链接默认库
    
            // link参数.
            linkerOpts 'Some linker option', 'More linker options'
    
            // build时打印所有参数.
            dumpParameters true
    
            // 计算编译各阶段时间.
            measureTime true
    
            // 编译过程中依赖的其他任务.
            dependsOn anotherTask
    
            // 传递给编译器的额外命令行参数.
            extraOpts '--time', '--verbose', 'linker'
    
            // 额外编译Linux平台的.
            target('linux') {
                // 输出路径,默认为 <baseDir>/<target>
                destinationDir 'exact/output/path'
    
                // 可以添加和上面类似的其他配置.
            }
        }
    
        library('bar') {
            // 其他参数的配置与上面一样
            // 生成bar.klib文件,默认输出路径为 build/konan/libs
        }
    
        bitcode('baz') {
            // 其他参数的配置与上面一样
            // 生成baz.bc文件,默认输出路径为 build/konan/bitcode
        }
    
        dynamic('quux') {
            // 其他参数的配置与上面一样
            // 生成quux.dll和quux_api.h文件,默认输出路径为 is build/konan/bin
        }
    
        framework('quuux') {
            // 其他参数的配置与上面一样
            // Object-C framework文件,Windows下不支持,默认输出路径为 build/konan/bin
        }
    
        interop('qux') {
            // native API的描述文件,默认路径为 src/main/c_interop/<interop-name>.def
            defFile project.file("deffile.def")
    
             // 头文件.
            headers project.files('header1.h', 'header2.h')
    
            // 其他属性配置同上
    
            // 头文件目录.
            includeDirs {
                allHeaders 'path1', 'path2'
    
                // 根据.def文件中定义的 'headerFilter' 筛选出来的头文件.
                headerFilterOnly 'path1', 'path2'
            }
            // 目录下的所有头文件.
            includeDirs "include/directory" "another/directory"
    
            // 需要额外链接的文件.
            link <files which will be linked with native stubs>
        }
    }

    运行下面的命令或双击Intellij中的build选项进行编译

    ./gradlew build

    这里写图片描述
    编译完之后就可以在build/konan目录下发现生成的目标文件了。
    这里写图片描述
    因为是直接把kotlin代码直接编译成了机器代码,因此可以脱离虚拟机直接运行。
    这里写图片描述

    使用Kotlin和C混合编程

    新建一个src/main/c目录用于放置c文件和.h头文件,用c语言编写一个函数给kotlin调用,代码如下:
    这里写图片描述

    //kotlinor.h
    #ifndef KOTLINOR_H
    #define KOTLINOR_H
    int add(int a, int b);
    #endif
    //kotlinor.c
    #include "kotlinor.h"
    #include <stdio.h>
    int add(int a, int b){
        return a + b;
    }
    //Main.kt
    
    //导入的包名和下面konanArtifacts中配置的一致
    import myPkg.*
    fun main(args: Array<String>) {
        println(add(1, 3))
    }

    使用下面的命令通过c代码生成.bc(BitCode)文件供下面使用

    clang -std=c99 -c kotliner.c(c文件) -o kotliner.bc(生成的bc文件) -emit-llvm

    根据上面的介绍配置build.gradle,修改konanArtifacts中的内容

    konanArtifacts {
        // 这个名字下面会用到,必须保持一致
        interop('myInterop') {
            // kotlin代码中import的包名
            packageName 'myPkg'
            compilerOpts '-Isrc/c'
            // null.def 中没有添加配置,可以按自己需求添加
            defFile 'null.def'
            headers "src/main/c/kotlinor.h"
            includeDirs "src/main/c"
        }
    
        program('foo') {
            srcFiles 'src/main/kotlin/Main.kt'
            // 上一个步骤生成的bc文件
            nativeLibraries 'src/main/c/kotlinor.bc'
    
            libraries {
                // interop的名字
                artifact 'myInterop'
            }
        }
    }

    同样是使用./gradlew build命令编译,就可以在build目录下顺利生成目标文件了。
    这里写图片描述

    这里写图片描述

    PS

    • 第一手最新资料请参考jetbrains的github
      https://github.com/JetBrains/kotlin-native

    • 因为Kotlin/Native编译器是将kotlin编译成机器码的编译器,而不是运行在JVM上的字节码,所以在这里的kotlin代码中不能使用Java sdk里的API。

    • 生成的可执行文件,windows下后缀为exe、Linux下后缀为kexe。windows下可以用直接双击运行,如果程序运行时间很短的话只会看到一个黑框一闪而过。

    展开全文
  • 使用Kotlin Native技术开发iOS应用

    千次阅读 2018-06-09 09:22:09
    AppCode简介 AppCode是JetBrains公司出品的用于开发Mac OS X以及iOS应用程序的集成开发环境,能够支持Xcode项目的运行和调试功能,并为Swift和Objective-C提供广泛的技术支持。目前,AppCode只能运行在Mac OS X操作...
  • 当你还在死扣泛型语法的时候,别人的文章早就说了Kotlin/NativeKotlin1.3的新特性”。瞬间感觉自己out了,今天我们就说说这些时髦的东西,也许你能看到一些和别人不一样的东西哦。 前段时间你们的熊猫小哥哥(也...
  • Kotlin Native 详细体验,你想要的都在这儿
  • 在这篇博文中,我们将讨论Kotlin/Native应用程序的开发。在这里,我们使用FFMPEG音频/视频解码器和SDL2进行渲染,来开发个简易的视频播放器。希望此文可以成为能对Kotlin/Native开发者有价值的开发指南,同时该文也...
  • Kotlin/Native is a technology for compiling Kotlin code to native binaries, which can run without a virtual machine. It is an LLVM based backend for the Kotlin compiler and native implementation of t....
  • Kotlin 语言的迷弟迷妹儿们有福了。就在前两天,官方正式宣布:Kotlin/Native 插件开始支持 AppCode!这里简单介绍一下两个关键字,可能很多人不是很熟悉 Kotlin...
  • 解读 Kotlin/Native 技术预览版

    千次阅读 2017-05-18 23:32:18
    解读 Kotlin/Native 技术预览版
  • Kotlin/Native尝试

    2019-03-16 17:47:55
    Kotlin/Native尝试 在官网看到Kotlin/Native已经达到1.0 Beta版于是就去尝试了一下,结果发现坑还是挺多的。首先Kotlin/JVM很多库是用不了的,这个已经猜到了。官网说已经预先导入了 POSIX、 gzip、 OpenGL、 Metal...
  • kotlin/native绑定到libui c library.libui是一个c轻量级多平台ui库,在linux(gtk3)、macos和windows上使用本机小部件。使用此绑定,您可以开发跨平台的、看起来像本机的gui程序,用kotlin编写,并编译为小型本机...
  • Kotlin/Native Kotlin/NativeKotlin的本地化构建工具,可以为Kotlin提供C的互操作性以及将Kotin开发应用编译成本地化的可执行程序。 Kotlin/Native 支持mac、linux、win32等多个平台,本文主要将以win32平台为主要...
1 2 3 4 5 ... 20
收藏数 5,395
精华内容 2,158
关键字:

kotlin/native