高可用架构公众号。 |
架构师之路 · 你的提示词根本只是在浪费算力,如何让deep ... · 2 天前 |
架构师之路 · 你的提示词根本只是在浪费算力,让deepse ... · 3 天前 |
架构师之路 · 90%的用户不知道!触发DeepSeek深度 ... · 3 天前 |
概述
1.1 Kotlin 多平台的发展历程
1.2 Kotlin Native 简介
Kotlin Native 是指将 Kotlin 源代码编译为目标平台的本地二进制可执行程序或库,以类似于 C/C++、Go 等语言的方式运行在目标平台的原生环境中。与 Kotlin JVM 和 Kotlin JS 相比,Kotlin Native 在语言本身上没有什么特殊之处,只是目标产物不同而已。
Kotlin Native 支持多种平台,包括 Android(NDK)、iOS、Linux、Windows(MinGW)、macOS 等,可以覆盖绝大多数消费终端的开发场景。事实上,在早期的版本中,WebAssembly 也曾是 Kotlin Native 支持的平台之一,不过 Kotlin WASM 的后端编译器已经基于新版架构重写,成为与 Kotlin Native 并列的独立目标平台。
Kotlin Native 运行时提供了内存垃圾回收机制,使得 Kotlin Native 程序的开发体验与 Kotlin JVM 一致。Kotlin Native 还提供了与 C、Objective-C 的互调用接口,可以安全方便地实现跨语言调用,进而充分利用平台的原生能力。
本文将基于 Kotlin 2.0.0 版本从编译时和运行时两个角度介绍 Kotlin Native 的关键技术和核心特性。
编译与产物
Kotlin 编译器包含两个部分,即负责将 Kotlin 源代码编译成 Kotlin IR 的前端部分(Front-end)和将 Kotlin IR 编译成目标文件的后端部分(Back-end)。Kotlin Native 的编译流程如图所示:
接下来我们用一个非常简单的例子来展示各阶段的编译结果。
2.1 前端编译与 Kotlin IR
fun main() {
println("Hello World!!")
}
FILE fqName:
fileName:Main.kt FUN name:main visibility:public modality:FINAL <> () returnType:kotlin.Unit
BLOCK_BODY
CALL 'public final fun println (message: kotlin.Any?): kotlin.Unit declared in kotlin.io' type=kotlin.Unit origin=null
message: CONST String type=kotlin.String value="Hello World!!"
2.2 后端编译与 LLVM IR
@main = alias i32 (i32, i8**), i32 (i32, i8**)* @Konan_main
define i32 @Konan_main(...) #11 {
%3 = tail call i32 @Init_and_run_start(...)
ret i32 %3
}
define i32 @Init_and_run_start(...) ... {
...
; 创建 Kotlin Native 运行时
tail call void @Kotlin_initRuntimeIfNeeded() #9
...
; 注意这里调用 @Konan_start
%11 = invoke i32 @Konan_start(...) ...
...
; 销毁 Kotlin 运行时
call void @Kotlin_shutdownRuntime()
...
}
define internal i32 @Konan_start(...) ... {
...
entry:
; 调用开发者定义的 main 函数
invoke void @"kfun:#main(){}"() #57 ...
...
}
define internal void @"kfun:#main(){}"() #6 !dbg !14199 {
...
entry:
; 调用 println("Hello World!!")
call void @"kfun:kotlin.io#println(kotlin.Any?){}"(...), ...
...
}
@1012 = internal unnamed_addr constant {
...,
[13 x i16] [
i16 72, i16 101, i16 108, i16 108, i16 111,
i16 32, i16 87, i16 111, i16 114, i16 108,
i16 100, i16 33, i16 33
]
}
由于 Kotlin 的字符是采用 UTF-16 编码的,每个字符占两个字节,因此 13 个 i16 正好对应于 13 个字符。
LLVM IR 可以直接对应到最终可执行程序中的指令,因此我们可以非常完整地观察看到 Kotlin Native 的 main 函数调用前后分别做了哪些准备和清理工作。此外,阅读 LLVM IR 也对于我们理解和认识 Kotlin 对象的内存布局有很大的帮助。
最后,LLVM 编译器会 将 LLVM IR 编译成对应平台的可执行程序或库,至此 Kotlin Native 的编译工作就全部完成了。
内存布局
3.1 基本数值类型的内存布局
val a = 1
var b: Float = 2f
var c: Double = a * 2 + b.toDouble()
val d = 'a'
val e: Short = 28
3.2 对象的内存布局
与 Java 类似,Kotlin 也存在对基本数值类型的装箱和拆箱的设计。不同之处在于,Kotlin 的装箱和拆箱是隐式的,开发者无须在代码编写时关心装箱和拆箱,编译器会根据实际的使用情况来决定是否需要装箱并保证尽可能不装箱。尽管与 Kotlin JVM 在实现上有不少差异,但显然 Kotlin Native 也存在装箱和拆箱。
例如:
val value: Double = 4.0
println(value)
value 作为基本数值类型,在栈上只占用 8 字节。随后我们将其传入 println 中,Kotlin 编译器就会生成相应的装箱代码,在堆上开辟一个 Double 类型的对象作为 println 的实参,这个对象占 24 字节的内存。
堆内存中开辟的对象主要包含三个部分,即堆对象链表节点 GC::ObjectData,对象头 ObjHeader,对象体。其中:
堆对象链表节点 GC::ObjectData 用于内存回收时的标记阶段,包含一个指向下一个对象的 GC::ObjectData 的指针,占 8 字节。
对象头 ObjHeader 包含了对象的类型信息 TypeInfo 的指针,占 8 字节,它的定义如下:
struct ObjHeader {
TypeInfo* typeInfoOrMeta_;
...
}
对象体就是对象的值本身,包含对象的所有字段的值。
示例中 Double 对象的对象体就是 Double 数值 4.0,占 8 字节。Double 在堆内存中的内存布局示意如下:
data class User(val id: Int, val name: String)
data class Repository(val id: Long, val name: String, val owner: User)
---
val user = User(1, "bennyhuo")
val repository = Repository(42, "kotlin-ir-printer", user)
println(repository)
%"kclassbody:User#internal" = type struct.ObjHeader, %struct.ObjHeader*, i32 }>
%"kclassbody:Repository#internal" = type struct.ObjHeader, i64, %struct.ObjHeader*, %struct.ObjHeader* }>
注意,这里只是类型的内存布局定义,因此不包含 GC::ObjectData 字段。
此外,User 类的 id 和 name 字段在编译之后顺序发生了变化,这实际上是编译器针对对象内存布局进行字段对齐优化之后的结果。如果我们想要整体禁止这项优化,可以在编译时传入 -Xbinary=packFields=false;如果想要单独禁止对某个类进行对齐优化,可以使用 @NoReorderFields 注解对这个类进行标注。不过,由于 @NoReorderFields 是 internal 的注解,因此使用时还需要压制相应的可见性报错,例如:
@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
import kotlin.native.internal.NoReorderFields
@NoReorderFields
data class User(val id: Int, val name: String)
3.3 数组的内存布局
数组也是对象,它的对象体其实就是数组的值,它的对象头除了类型信息以外,还包含数组元素的个数。即:
数组元素的大小由数组元素的类型决定,因此数组的对象体的大小其实就是
sizeof(T) * count_
,其中,
sizeof(T)
定义在数组元素的类型信息当中。
ObjHeader
和
count_
合起来构成了
ArrayHeader
,它的定义如下:
struct ArrayHeader {
TypeInfo* typeInfoOrMeta_;
uint32_t count_;
...
};
字符串也是数组类型,我们可以把字符串类型理解为
Array
。接下来我们以字符串为例详细介绍数组的内存布局。
在编译与产物一节中我们已经见识到了 Kotlin 字符串常量的 LLVM IR,字符串常量 "Hello World!!" 编译后生成的完整 LLVM IR 如下:
@1012 = internal unnamed_addr constant {
%struct.ArrayHeader, [13 x i16]
} {
%struct.ArrayHeader {
%struct.TypeInfo* bitcast (
i8* getelementptr (i8, i8* bitcast ({ %struct.TypeInfo, [3 x i8*] }* @"kclass:kotlin.String" to i8*), i32 1) to %struct.TypeInfo*
),
i32 13
},
[13 x i16] [
i16 72, i16 101, i16 108, i16 108, i16 111, i16 32, i16 87,
i16 111, i16 114, i16 108, i16 100, i16 33, i16 33
]
}
ArrayHeader
和一个
i16
整型的数组,这里的
i16
实际上就对应于 Kotlin 的
Char
类型。它的内存布局如下表所示:
3.4 类型信息 TypeInfo
编译器在编译时根据 Kotlin IR 中的类型信息生成
TypeInfo
。
TypeInfo
的定义如下,其中
writableInfo_
用于支持与 Objective-C 的互调用:
struct TypeInfo {
const TypeInfo* typeInfo_;
const ExtendedTypeInfo* extendedInfo_;
uint32_t unused_;
int32_t instanceSize_;
const TypeInfo* superType_;
const int32_t* objOffsets_;
int32_t objOffsetsCount_;
const TypeInfo* const* implementedInterfaces_;
int32_t implementedInterfacesCount_;
int32_t interfaceTableSize_;
InterfaceTableRecord const* interfaceTable_;
ObjHeader* packageName_;
ObjHeader* relativeName_;
int32_t flags_;
ClassId classId_;
#if KONAN_TYPE_INFO_HAS_WRITABLE_PART
WritableTypeInfo* writableInfo_;
#endif
const AssociatedObjectTableRecord* associatedObjects;
void (*processObjectInMark)(void* state, ObjHeader* object);
uint32_t instanceAlignment_;
...
};
String
为例,运行时
String
的
TypeInfo
的内存信息如下:
注意,数组和字符串的
instanceSize_
字段是元素大小的相反数,例如这里字符串的元素大小为 2 字节,
instanceSize_
的值就是
0xFFFFFFFE
,也就是 -2。
packageName_
是类的包名,这里是 kotlin;
relativeName_
是类型的类名,这里是
String
。在调试 Kotlin Native 的源码时,我们可以通过打印这两个字段来确认
TypeInfo
对应的实际类型。顺便提一句,
KClass
的
simpleName
和
qualifiedName
就是基于这两个字段实现的:
internal class KClassImpl<T : Any>(private val typeInfo: NativePtr) : KClass
{ override val simpleName: String?
get() = getRelativeName(typeInfo, true)?.substringAfterLast('.')?.substringAfterLast('$')
override val qualifiedName: String?
get() {
val packageName = getPackageName(typeInfo, true) ?: return null
val relativeName = getRelativeName(typeInfo, true) ?: return null
return if (packageName.isEmpty()) relativeName else "$packageName.$relativeName"
}
...
}
vtable 的含义与 C++ 相同,用于存储虚函数的地址。与 C++ 的不同之处在于,在 Kotlin 当中,open 的函数都可以理解为虚函数,子类覆写父类的函数地址都需要存储于 vtable 中。接下来我们简单介绍一下虚函数的生成方式。
在 Kotlin 中,只有抽象类和密封类的类型信息不包含 vtable,因为我们无法实例化这些类型。对于可以被实例化的类型,其 vtable 包含以下内容:
父类的 vtable,如果当前类型中覆写了父类的函数,将父类对应的函数替换成覆写的函数。
当前类中可以被覆写的函数。
例如:
class A {
fun a () { }
override fun toString(): String {
return super.toString()
}
}
它的 vtable 包含以下函数:
kfun:kotlin.Any#equals(kotlin.Any?){}kotlin.Boolean
kfun:kotlin.Any#hashCode(){}kotlin.Int
kfun:A#toString(){}kotlin.String
这三个函数实际上是继承自 Any 的 vtable,注意 toString 被替换成了 A 当中覆写的版本。
再例如:
open class B {
open fun b1() {}
open fun b2() {}
fun b3() {}
}
open class C : B() {
final override fun b1() {}
}
class D : B()
class E : C() {
override fun b2() {}
}
kfun:kotlin.Any#equals(kotlin.Any?){}kotlin.Boolean
kfun:kotlin.Any#hashCode(){}kotlin.Int
kfun:kotlin.Any#toString (){}kotlin.String
kfun:B#b1(){}
kfun:B#b2(){}
kfun:kotlin.Any#equals(kotlin.Any?){}kotlin.Boolean
kfun:kotlin.Any#hashCode(){}kotlin.Int
kfun:kotlin.Any#toString(){}kotlin.String
kfun:C#b1(){}
kfun:B#b2(){}
kfun:kotlin.Any#equals(kotlin.Any?){}kotlin.Boolean
kfun:kotlin.Any#hashCode(){}kotlin.Int
kfun:kotlin.Any#toString(){}kotlin.String
kfun:B#b1(){}
kfun:B#b2(){}
kfun:kotlin.Any#equals(kotlin.Any?){}kotlin.Boolean
kfun:kotlin.Any#hashCode(){}kotlin.Int
kfun:kotlin.Any#toString(){}kotlin.String
kfun:C#b1(){}
kfun:E#b2(){}
内存管理
Kotlin Native 对象的内存管理方式与 Java 对象的类似,也采用垃圾回收机制实现对象内存的自动管理。
4.1 内存回收调度策略
4.2 内存垃圾回收算法
4.3 内存分配方式
内存的默认分配方式会随着内存回收算法和其他配置发生变化。在 2.0 以前的一段时间里,默认的内存分配方式会优先选择 mimalloc,如果内存回收算法是 ptms,则会选择 custom alloc。如果生产环境中实测某一种分配方式有更高的性能,建议直接在项目配置中显式配置这种分配方式,不要依赖默认配置。
Kotlin Native 内存管理机制仍然有不小的提升空间,减少 GC 带来的程序停顿是内存管理机制提升的重要目标。除了可以持续优化内存分配方式和回收算法以外,Kotlin Native 实际上也可以推出分代内存管理机制,届时 Kotlin Native 在应对大量的浮动内存时将更加游刃有余。
跨语言调用
5.1 符号关系
要实现与其他语言的互调用,我们需要解决两个问题:
对象的内存布局。
函数的名字修饰。
前面已经讨论过,Kotlin Native 对象的内存布局与 C 的结构体的内存布局没有概念上的差异,我们完全可以以 C 语言的视角去理解 Kotlin Native 对象的内存。 因此,本节我们将专注于探讨函数的名字修饰。
5.1.1 导出 C 符号
namespace NS {
void func() {}
void func(int, float ) {}
};
_ZN2NS4funcEv
_ZN2NS4funcEif
package com.tencent.kotlin.sample
fun func() { }
fun func(i: Int, f: Float) { }
_kfun:com.tencent.kotlin.sample#func(){}
_kfun:com.tencent.kotlin.sample#func(kotlin.Int;kotlin.Float){}
与 GCC 编译器为 C++ 实现的名字修饰不同,Kotlin 的修饰规则非常清晰易懂。不过,问题也是很明显的,我们要想与 C 语言互调用,生成的符号必须让 C 语言能够理解。
C++ 通过使用 extern "C" 修饰函数使得函数本身采用 C 语言的方式生成符号,例如:
extern "C" void func() {}
这样函数 func 经过编译之后生成的符号名就是 func。
Kotlin 也提供了类似的机制,即 @CName 注解。
@CName("func")
fun func() { }
extern "C" void _konan_function_0();
RUNTIME_USED extern "C" void func() {
Kotlin_initRuntimeIfNeeded();
ScopedRunnableState stateGuard;
FrameOverlay* frame = getCurrentFrame();
try {
_konan_function_0();
} catch (...) {
SetCurrentFrame(reinterpret_cast
(frame)); HandleCurrentExceptionWhenLeavingKotlinCode();
}
}
5.1.2 导出 Objective-C 符号
Kotlin 与 Objective-C/Swift 互调用时,需要导出 Objective-C/Swift 符号。例如:
Funcs.kt
fun func() { }
fun func(i: Int, f: Float) { }
fun func2(i: Int, f: Float) { }
__attribute__((swift_name("FuncsKt")))
@interface FrameworkFuncsKt : FrameworkBase
+ (void)func __attribute__((swift_name("func()")));
+ (void)funcI:(int32_t)i f:(float )f __attribute__((swift_name("func(i:f:)")));
+ (void)func2I:(int32_t)i f:(float)f __attribute__((swift_name("func2(i:f:)")));
@end
请注意,这里生成的类名是
FrameworkFuncsKt
,其中
Framework
对应于模块名
framework
,
FuncsKt
对应于文件名
Funcs.kt
,
__attribute__((swift_name("func(i:f:)")))
则为函数指定了 Swift 函数名,方便在 Swift 中调用。
这里采用的是默认的映射规则,生成的 Objective-C 函数的函数名为:
Kotlin 函数:func(i: Int, f: Float)
Objective-C 符号:
[<1st param name#capitalize>:<2nd param name>:...] Objective-C 符号:func I : f :
Swift 符号:
([<1st param name>:<2nd param name>:...]) Swift 符号:func ( i : f : )
@ObjCName(swiftName = "MySwiftArray")
class MyKotlinArray {
@ObjCName("index")
fun indexOf(@ObjCName("of") element: String): Int = 1
}
__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("MySwiftArray" )))
@interface FrameworkMyKotlinArray : FrameworkBase
- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer));
+ (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead")));
- (int32_t)indexOf:(NSString *)of __attribute__((swift_name("index(of:)")));
@end
@ObjCName( "MySwiftArray")
class MyKotlinArray {
@ObjCName("index")
fun indexOf(@ObjCName("of") element: String): Int = 1
}
__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("MySwiftArray")))
@interface FrameworkMySwiftArray : FrameworkBase
- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer));
+ (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead")));
- (int32_t)indexOf:(NSString *)of __attribute__((swift_name("index(of:)")));
@end
@ObjCName("MySwiftArray" , exact = true)
class MyKotlinArray {
@ObjCName("index")
fun indexOf(@ObjCName("of") element: String): Int = 1
}
__attribute__((objc_subclassing_restricted))
@interface MySwiftArray : FrameworkBase
- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer));
+ (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead")));
- (int32_t)indexOf:(NSString *)of __attribute__((swift_name("index(of:)")));
@end
事实上, exact 参数会强制生成的符号名为注解中声明的名字,否则最终生成的类名前会增加模块名和外部类的前缀。
作为对比,如果不加注解,默认情况下生成的符号如下:
__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("MyKotlinArray")))
@interface FrameworkMyKotlinArray : FrameworkBase
- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer));
+ (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead")));
- (int32_t)indexOfElement:(NSString *)element __attribute__((swift_name("indexOf(element:)")));
@end
5.1.3 Objective-C 的符号冲突
在前面的分析中,我们看到了 C++ 的命名空间和 Kotlin 的包名对符号名字修饰的影响。现代编程语言大多数有命名空间的概念,命名空间一方面可以提供可见性约束,另一方面也能有效地解决符号冲突的问题。
不过,Objective-C 没有命名空间的概念,因此 Kotlin 类、函数在导出 Objective-C 符号时就会面临符号冲突的问题。
例如在模块 frameowork 中定义以下类和函数:
// com/bennyhuo/kotlin/A.kt
package com.bennyhuo.kotlin
class A
fun a() {}
----
// com/bennyhuo/kotlin/native/A.kt
package com.bennyhuo.kotlin.native
class A
fun a() {}
__attribute__((swift_name("AKt")))
@interface FrameworkAKt : FrameworkBase
+ (void)a __attribute__((swift_name("a()")));
+ (void)a_ __attribute__((swift_name("a_()")));
@end
__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("A")))
@interface FrameworkA : FrameworkBase
...
@end
__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("A_")))
@interface FrameworkA_ : FrameworkBase
...
@end
在这里,Kotlin 通过为其中一个类的类名添加下划线以实现符号冲突的避让。
除了全局符号冲突以外,还有同名属性和函数的冲突。这类符号冲突事实上也存在于 Kotlin 与 Java 互调用的情况,例如:
// A.kt
interface A {
fun a()
}
interface B {
fun a(): String
}
// Main.java
public class Main implements A, B {
@Override
public void a() {}
^^^^
e: 'a()' in 'Main' clashes with 'a()' in 'B'; attempting to use incompatible return type
}
Java 编译器有能力对符号冲突做出判断,因此 Kotlin 编译器无需担心定义了同名函数的类或接口被同一个 Java 类实现或者继承。
而在 Objective-C 中,情况就变得有一些微妙了:
@protocol A <NSObject>
@required
-(void) a;
@end
@protocol B <NSObject>
@required
-(int) a;
@end
@interface Main : NSObject<A, B>
@end
@implementation Main
- (void) a {
}
- (int) a {
^^^^^^^^^
e: Duplicate declaration of method 'a'
return 1;
}
@end
@implementation Main
- (void) a {
}
@end
Main* main = [[Main alloc] init];
id b = main;
int value = [b a];
NSLog(@"%d\n", value);
interface A {
fun a()
fun b(i: Int)
fun c(f: Float)
fun d(d: Double)
fun e(): Boolean
}
interface B {
fun a(): String
fun b(i: Int)
fun c (i: Int)
@ObjCName("d")
fun d0(d: Double)
val e: Boolean
}
open class C {
fun a(): Int = 1
open fun b(i: Int): String = "b"
fun c(i: Float) {}
private fun d(d: String) {}
}
class D {
fun a(): Int = 1
fun b(i: Int): String = "b"
fun c(f: Float) {}
fun d(d: Double) {}
}
fun A.b(i: Int) {}
fun B.b(i: Int) {}
其中:
A#a、B#a 和 C#a 冲突,因为三者的返回值类型不同。需要说明的是,尽管在 Kotlin 中,C#a 是 final 函数,不能够被覆写,但导出到 Objective-C 符号之后,函数的 final 信息会丢失,因此 C#a 也是会存在符号冲突问题的,而 D#a 不会冲突,因为 D 不可以被继承。
A#b 和 B#b 不会冲突,因为它们的函数名、参数类型、返回值类型完全相同。C#b 与前面二者会产生冲突,因为 C#b 的返回值类型与它们不同。
A#c 的符号名为 cF:,B#c 的符号名为 cI:,符号名不同,因此二者不会冲突。不过,C#c 的符号名也为 cI:,但参数类型与 B#c 不同,因此会产生冲突。
A#d 和 B#d0 也会产生冲突,尽管二者对应的 Kotlin 函数名不同,但最终的符号名相同,这是一个非常微妙的 case,我们在稍后进一步展开讨论。C#d 是私有函数,不会导出符号,因此不会产生冲突。
A#e 和 B#e、C#e 的 getter 会产生冲突,导致属性的 getter 被隐藏。B#e 和 C#e 两个属性也会冲突,因为类型不同。
最后的两个扩展函数 A#b 和 B#b 也会产生冲突,因为它们会被导出为基于 Kotlin 文件生成的类的两个静态函数。
接下来我们再举一个例子对前面提到的 A#d 与 B#d0 的冲突产生的原因进行进一步分析:
interface A {
fun a()
}
interface B {
@ObjCName("a")
fun b()
}
__attribute__((swift_name("A")))
@protocol FrameworkA
@required
- (void)a __attribute__((swift_name("a()")));
@end
__attribute__((swift_name("B")))
@protocol FrameworkB
@required
- (void)a_ __attribute__((swift_name("a_()")));
@end
open class C : A, B {
override fun b() {}
override fun a() {}
}
@interface FrameworkC : FrameworkBase
...
- (void)a __attribute__((swift_name("a()")));
- (void)a_ __attribute__((swift_name("a_()")));
@end
与类名相同,Kotlin 编译器在遇到属性、函数符号冲突时会默认通过为后参与编译的属性、函数的符号名添加下划线的方式来进行冲突避让。这样做的好处就是我们很少需要关心 Kotlin 符号的冲突问题,坏处就是我们在 Objective-C 中调用 Kotlin 导出的符号时总是需要小心因为冲突避让而产生的 Kotlin 模块的 ABI 变化。
我们可以通过配置编译参数
-Xbinary=objcExportIgnoreInterfaceMethodCollisions=true
来忽略定义在不同类和接口的同名属性和函数所产生的冲突,这样上述示例中 1~5 的冲突 case 可以被忽略。不过,忽略问题只会为项目的稳定性带来风险,Kotlin 编译器在 2.0 版本中新增了两个编译参数 -
Xbinary=objcExportReportNameCollisions=true
和
-Xbinary=objcExportReportNameCollisions=true
,可以对分别启用对符号冲突的 case 报警或者报错,方便我们在项目中尽早发现符号冲突的问题。以下是报警信息示例:
w: name is mangled when generating Objective-C header
(at fun a(): String defined in com.bennyhuo.kotlin.sample.B)
w: name is mangled when generating Objective-C header
(at fun d0(d: Double): Unit defined in com.bennyhuo.kotlin.sample.B)
w: name is mangled when generating Objective-C header
(at fun `
`(): Boolean defined in com.bennyhuo.kotlin.sample.B)
导出的 Swift 符号的冲突判断条件与 Objective-C 符号类似,我们可以通过编译器参数
-Xbinary=objcExportDisableSwiftMemberNameMangling=true
来忽略 Swift 符号冲突。
事实上,Kotlin 在与 Java 的互调用时也会经常产生符号冲突,不过由于 Java 与 C、C++ 和Objective-C 的抽象层次不同,Java 编译器能够在编译时把绝大多数的冲突问题暴露出来,因此 Kotlin 没有提供相应的冲突避让策略。Swift 的编译器同样能有效地识别符号冲突的能力,因此理论上在生产环境中可以直接忽略 Swift 的符号冲突,依赖 Swift 编译器对冲突的符号进行检查。我们同样以前面提到的 Objective-C 协议为例:
@protocol A <NSObject>
@required
-(void) a;
@end
@protocol B <NSObject>
@required
-(int) a;
@end
class SwiftMain: NSObject, A, B {
func a() { }
func a () -> Int32 { 0 }
^^
e: Method 'a()' with Objective-C selector 'a' conflicts with previous declaration with the same Objective-C selector
}
class SwiftMain: NSObject, A, B {
^^^^^^^^^
e: type 'SwiftMain' does not conform to protocol 'B'
func a() { }
}
相比之下,Swift 编译器比 Objective-C 编译器的检查更加严格了。
当然,从生产实践的角度而言,我们应该尽可能避免和减少导出 Kotlin 模块的符号,同时对于导出的符号进行严格的版本控制,以避免因符号冲突避让或者符号的其他变更导致 Objective-C 和 Swift 的调用处出现异常。
5.2 类型的映射
跨语言调用就是解决两个问题:数据和函数的映射。数据类型又分为语言提供的基本类型(数值类型、字符串等)和自定义类型(例如自定义的 struct、union 类型等)
5.2.1 数值类型
我们在内存布局一节曾经提到,Kotlin 的基本数值类型的内存布局与 C 语言的数值类型完全相同,因此映射关系也非常简单直接:
5.2.2 字符串类型
字符串类型的映射稍微有些复杂,C 语言中的字符串其实就是以字符 \0 结尾的字符数组,由于一个字符的只占用 1 字节,因此可以表示的范围实际上是 0~255,即 ASCII 字符集及其扩展字符集。C 语言标准中没有规定字符串采用什么编码,字符串字面量的编码取决于代码文件的字符编码,字符串的编码则取决于它的来源。通常我们在 Windows 开发环境中使用 MSVC 编译器,字符串字面量采用 GBK 编码,而在 macOS 和 Linux 环境中使用 GCC 和 Clang 编译器,字符串字面量采用 UTF-8 编码。
Kotlin 的字符串 String 采用了类似于 Java 的设计思路,字符编码采用 UTF-16,一个常见字符占用 2 字节;一个增补 Unicode 字符则采用 Surrogate Pairs 存储,占用 4 字节。
由此可见,Kotlin 与 C 语言的字符串之间想要转换,还涉及字符串的编码转换。为了降低复杂度,Kotlin 在设计时约定互调用的 C 语言字符串采用 UTF-8 编码。如果读者熟悉 C/C++ 中的宽字符
wchar_t
类型,我们可以将 Kotlin 字符串转换成 C 语言字符串的过程类比成宽字符串(
wchar_t *
)转换成窄字符串(
char *
)的过程(调用 C 标准库的
wcstombs
函数)。
这个转换过程通常是隐式的,我们在 C 语言中调用 Kotlin 函数,可以直接传入一个
char *
,Kotlin 运行时会自动将这个
char *
转换为 Kotlin 的
String
。例如:
fun kFunc(s: String) {}
编译之后会生成下面的包装函数:
extern "C" void _konan_function_3(KObjHeader*);
static void _konan_function_3_impl(const char* arg0) {
Kotlin_initRuntimeIfNeeded();
ScopedRunnableState stateGuard;
FrameOverlay* frame = getCurrentFrame();
KObjHolder arg0_holder;
try {
_konan_function_3(CreateStringFromCString(arg0, arg0_holder.slot()));
} catch (...) {
SetCurrentFrame(reinterpret_cast
(frame)); HandleCurrentExceptionWhenLeavingKotlinCode();
}
}
_konan_function_3
就是
kFunc
函数编译之后的符号,而导出给 C 语言的实际上是
_konan_function_3_impl
,
CreateStringFromCString
是定义在 Kotlin Native 运行时的函数,它的功能就是对字符串做编码转换。反之亦然。
在与 Objective-C 互调用时,Objective-C 的 NString 与 Kotlin 的 String 也存在隐式地转换逻辑。NSString 提供了相应的 API 实现编码的转换,因此,NString 与 Kotlin String 的转换逻辑相对简单一些,参见:
Kotlin_Interop_CreateKStringFromNSString
和
Kotlin_ObjCExport_CreateRetainedNSStringFromKString
。
5.2.3 自定义类型
C 语言的 struct 和 union 会被映射成 Kotlin 的 class。例如:
typedef union {
int i;
char cs[16];
} Tag;
typedef struct {
int id;
const char* name;
} User;
typedef struct {
int id;
User owner;
} Repository;
映射之后的结果如下:
class Tag (rawPtr: NativePtr) : CStructVar {
companion object : CStructVar.Type
val cs: CArrayPointer
var i: Int
}
class User(rawPtr: NativePtr) : CStructVar {
companion object : CStructVar.Type
var id: Int
var name: CPointer
? val tag: Tag
}
class Repository(rawPtr: NativePtr) : CStructVar {
companion object : CStructVar.Type
var id: Int
val owner: User
}
从这个映射关系中,我们可以发现几个细节:
struct
和
union
都可以认为是值类型,在 C 语言中,实例既可以在栈内存上创建,也可以在堆内存上创建。映射到 Kotlin 之后,实例的内存分配逻辑将交给 Kotlin 管理,因此我们看到这些类型的主构造器都有一个 NativePtr 类型的参数。
数值类型和指针类型的成员映射成了可变属性(
var
),而
struct
、
union
类型成员则映射成了只读属性(
val
)。以
Repository#owner
为例,从 Kotlin 的语法角度来理解,
owner
应该上是 User 类型的实例的引用,
Repository
的实例中并不会直接包含 User 实例的数据;而从 C 语言的角度来看,
owner
字段的内存是 Repository 的实例的一部分。对
owner
进行赋值,在 C 语言看来是将另一个 User 实例的数据复制过来,而在 Kotlin 看来,这仅仅是将
owner
指向的对象做修改。基于这个原因,
owner
被映射成只读属性主要是为了避免语义上出现歧义。我们可以通过
CValue
来实现数据的复制,这部分内容我们将在指针一节进行进一步讨论。
companion object
继承了
CStructVar.Type
,包含了类型需要的内存大小和对齐信息。在 Kotlin 代码中创建这些类型的实例时将会用到这些信息。
Objective-C 的
@protocol
则会被映射为 Kotlin 的
interface
,
@interface
的会被映射成 Kotlin 的
class
。例如:
#import
@interface TouchEvent
@end
@protocol OnTouchDelegate
-(void) onTouch:(TouchEvent*) event;
@end
@interface ObjcUser : NSObject
@property (readonly) int id;
@property NSString *name;
-(void) nameWithFirstName: (NSString *) firstName lastName:(NSString *) lastName;
+(NSString *) typeName;
@end
映射之后的结果如下:
interface OnTouchDelegateProtocolMeta : ObjCClass
interface OnTouchDelegateProtocol : ObjCObject {
abstract fun onTouch(event: TouchEvent?)
}
open class TouchEventMeta : ObjCObjectBaseMeta {
protected constructor()
}
open class TouchEvent : ObjCObjectBase {
companion object : TouchEventMeta, ObjCClassOf
protected constructor()
}
open class ObjcUserMeta : NSObjectMeta {
protected constructor()
open external fun alloc(): ObjcUser?
open external fun allocWithZone(zone: CPointer<cnames.structs._NSZone>?): ObjcUser?
open external fun new(): ObjcUser?
open external fun typeName(): String?
}
open class ObjcUser : NSObject {
companion object : ObjcUserMeta, ObjCClassOf
constructor()
val id: Int
var name: String?
open external fun id(): Int
open external fun init(): ObjcUser?
open external fun name(): String?
open external fun nameWithFirstName(firstName: String?, lastName: String?)
open external fun setId(id: Int)
open external fun setName(name: String?)
}
通过这个映射关系,我们可以发现:
Objective-C 的只读属性、可变属性分别映射成 Kotlin 的只读属性、可变属性。
Objective-C 类的非静态成员会被映射成 Kotlin 的属性和函数,静态成员则映射成对应 class 的 companion object 的属性和函数。
Objective-C 协议映射成 Kotlin 接口后,接口名会增加 Protocol 后缀。
此外,Objective-C 的 Category 成员会映射成 Kotlin 扩展函数,Kotlin 的扩展函数导出到 Objective-C 之后会映射成扩展函数所在文件生成的类的静态函数(类似于与 Java 互调用的情况)。事实上,Objective-C 和 Swift 的类在扩展时可以实现协议,这一点在 Kotlin 中无法做到。
Kotlin 的类和接口与 Objective-C 类和协议在一起使用时有一些限制,常见的限制列举如下:
Kotlin 接口不能继承 Objective-C 协议。
Kotlin 类或者对象(object)可以实现 Objective-C 协议或者继承 Objective-C 类,但该 Kotlin 类必须是 final 的。
实现了 Objective-C 协议或者继承了 Objective-C 类的 Kotlin 类的伴生对象不能有字段(Field),即不允许定义有幕后字段(backing-field)的属性。
实现了 Objective-C 协议或者继承了 Objective-C 类的 Kotlin 类不能同时继承 Kotlin 类或者实现 Kotlin 接口。
实现了 Objective-C 协议的 Kotlin 类必须继承自 Objective-C 类(例如 NSObject)。
实现了 Objective-C 协议或者继承了 Objective-C 类的 Kotlin 类不能导出 Objective-C 符号,即只能在 Kotlin 模块内部访问。
由于当前版本 Kotlin 与 Objective-C 的互调用存在较多限制,开发者需要结合具体的 Kotlin 版本做好类结构设计。当然,这些限制很有可能随着 Kotlin 与 Objective-C 的互调用能力的提升而逐步移除。
5.2.4 指针类型
C 语言中的指针类型可以用于访问对应地址的内存,它为开发者提供了更为底层的内存访问能力。Kotlin Native 设计了一整套类型来对应 C 语言的指针相关的类型。
我们先来看一下 CValuesRef 及其子类。
从命名上来看,CValuesRef 表示 C 语言的值的引用。我们在 C 语言中不会使用“引用”这样的术语,这里之所以有这样的类型结构,主要是为了统一数组和指针在概念上的抽象。各类型的详细说明如下:
其中,CValues 类型的值是不可修改的,因为 CValues 类型的值是分配在 Kotlin 堆内存上的,只有在做为 C 函数的实参时才会将值复制到 C 语言的堆内存上。
例如:
void set_ints(int *ints, int count) { ... }
在 Kotlin 中调用
set_ints
的方法如下:
// 相当于 C 中定义 int 数组:int ints[] = {1, 2, 3};
val ints = cValuesOf(1,2,3)
// 相当于 C 中 set_ints(ints, sizeof(ints));
// ints.size 是开辟的内存大小,不是值的个数
set_ints(ints, ints.size / sizeOf
().toInt())
CValue 主要用于读写结构体类型,例如:
typedef struct {
int id;
const char* name;
} User;
static User default_user = {
.id = 0,
.name = "admin",
};
在 Kotlin 中我们可以使用 CValue 来创建 User 的实例:
// user 的内存值为 0
val user: CValue
= cValue ()
// 相当于 User user2 = {.id = 1, .name="bennyhuo"};
val user2: CValue
= cValue { id = 1
"bennyhuo".cstr.place(name)
}
// 注意 default_user 是 C 中定义的静态变量
val user3: User = default_user
// 使用 default_user 初始化 user4
// 相当于 User user4 = default_user;
val user4: CValue
= default_user.readValue()
// 将 user2 的值写入到 default_user 的地址上,
// 相当于 default_user = user2;
user2.write(default_user.rawPtr)
// 获取 user4 的 id
// 相当于 int id4 = user4.id;
val id4 = user4.useContents { id }
// 获取 user4 的 name 的错误做法
// 不能在 useContents 中返回临时对象的指针
val name4 = user4.useContents { name } // ×
// 获取 user4 的 name 的正确做法
// 必须在 useContents 内部完整数据的复制和转换
val name4 = user4.useContents { name.toKString() } // √
请注意,示例中提供的 C 代码仅供对照参考,方便理解代码的逻辑,二者实际的内存分配过程并不相同。CValue 类型的对象是创建在 Kotlin 堆内存上的,只有使用时才会临时分配到 C 的堆内存上,这也意味着在 Kotlin 中频繁访问 CValue 对象总是会存在内存复制的开销。
需要强调的是,示例中的 useContents 函数在调用时会将 user4 的值复制到 C 的堆内存上,并且会在函数返回之后释放这部分内存,返回指向这部分临时内存的指针的行为是不安全的。因此在 useContents 中直接返回 name 相当于返回了临时对象的指针,正确的做法是在 useContents 内部完成数据的复制和转换。因此在 useContents 中直接返回 name 相当于返回了临时对象的指针,正确的做法是在 useContents 内部完成数据的复制和转换,然后再返回。
CValuesRef 引用的类型是 CPointed,它也有一整套继承结构如图所示:
CPointed 及其子类型用于描述引用或者指针解引用之后的变量类型,例如
int *
解引用之后的变量类型就是 int。
请注意,指针解引用之后的表达式是个左值。以 IntVar 为例,IntVar 类型的变量背后一定有一块存储 Int 的内存,因此 Kotlin 中的 Int 无法作为指针解引用之后的变量类型。我们可以通过 IntVar 的 value 属性来访问对应的内存空间:
val intVar: IntVar = ...
intVar.value = 10
CPointer 和 CPointed 之间可以相互转换,方法如下:
val intPtr: CPointer
= ... // 解引用,IntVar 是 IntVarOf
的别名 // 相当于 C 中的 int intVar = *intPtr;
val intVar: IntVar = intPtr.pointed
---
val intVar: IntVar = ...
// 取地址并创建指针变量
// 相当于 C 中的 int *intPtr = &intVar;
val intPtr: CPointer
= intVar.ptr
5.2.5 函数类型
C 语言的函数可以直接导出符号,映射成 Kotlin 函数声明,例如:
int add(int a, int b) {
return a + b;
}
映射之后的结果是:
external fun add(a: Int, b: Int): Int
反之亦然,我们在符号导出一节已经做过很多讨论,这里不再赘述。
接下来我们通过例子探讨一下函数类型的映射。
int add(int a, int b) { ... }
typedef int (*OpFuncPtr)(int, int);
typedef int OpFunc(int, int);
OpFuncPtr 和 OpFunc 映射到 Kotlin 之后的类型如下:
external fun add(a: Int, b: Int): Int
typealias OpFuncPtrVar = CPointerVarOf
typealias OpFuncPtr = CPointer
Int , Int) -> Int>>typealias OpFunc = CFunctionInt, Int) -> Int>
由此可见,Kotlin 使用 CFunction 类型来表示 C 语言中的函数类型,这与 KFunction 类型表示 Kotlin 函数类型的设计相对应。结合前面对指针类型的讨论,函数指针的类型自然就是
CPointer
了。OpFuncPtrVar 用来描述 OpFuncPtr 解引用之后的类型,这个符合指针类型的整体设计。
在 C 语言中,我们提到函数的类型通常就是指函数指针的类型,这主要是因为函数名总是会在表达式中隐式转换为指向自己的函数指针。例如:
OpFuncPtr op = add; // OK
OpFuncPtr op2 = &add; // OK
OpFuncPtr op3 = *add; // OK
OpFunc* op = add; // OK
OpFunc* op2 = &add; // OK
OpFunc* op3 = *add; // OK
函数 add 的类型是
int(int, int)
,不过我们总是可以把
add
当成
int (*)(int, int)
类型的函数指针来使用。
然而在 Kotlin 中,函数名和函数引用的转换是显式的行为,函数名只能用来调用函数,用作表达式时只能使用函数引用。
val opFunc: KFunction2<Int, Int, Int> = ::add // OK
val opFunc2: KFunction2<Int, Int, Int> = add // Error
val opFuncPtr: OpFuncPtr = ::add // Error
需要注意的是,C 函数映射成 Kotlin 函数声明之后,它的类型自然也就映射为 KFunction 了,因此我们不可以使用映射之后的 add 的函数引用来初始化 OpFuncPtr 类型的变量。如果想要在 Kotlin 中实例化 C 函数指针,需要使用 staticCFunction,这主要用于从 Kotlin 向 C 函数中传入函数指针参数的场景。例如:
int op(OpFuncPtr func, int a, int b) {
return func(a, b);
}
op 函数需要一个 OpFuncPtr 的函数指针,这个函数映射成 Kotlin 函数之后的声明如下:
external fun op(func: OpFuncPtr?, a: Int, b: Int): Int
在 Kotlin 调用这个函数的方法示例如下:
op(staticCFunction { a, b -> a + b }, 1, 2)
请注意 staticCFunction 的参数必须是 Kotlin 顶级函数或者没有捕获外部状态的 Lambda 表达式。
更多的情况下,我们可能需要在 C 中调用 Kotlin 类的成员函数,这时我们需要将 receiver 通过函数参数传递,这也是 C 函数回调常用的方式,例如:
typedef void (*Callback)(void* data, int value);
void set_callback(Callback callback, void* data) {
...
}
在 C 中,set_callback 可以用来设置回调,通常我们会把 callback 和 data 存起来,回调时将 data 透传给 callback 即可。而这个 data 就可以用来存储 Kotlin 函数的 receiver 信息。
class NativeEventCallback {
fun onEvent(value: Int) { ... }
}
---
val callback = NativeEventCallback()
val callbackRef = StableRef.create(callback)
set_callback(staticCFunction { data, value ->
data?.asStableRef
()?.get()?.onEvent(value) }, callbackRef.asCPointer())
在 Kotlin 中,我们可以为任意 Kotlin 对象创建一个 StableRef,顾名思义,通过 StableRef 我们可以获得一个稳定指针,通过这个指针我们可以总是在 StableRef 的生命周期范围内获取到对应的 Kotlin 对象。
在这里,data 就是通过 callbackRef 获取到的稳定指针,通过 asStableRef 函数转回 StableRef 进而获取到 callback 对象,完成 onEvent 的调用。
需要注意的是,StableRef 需要在使用完毕之后通过 dispose 函数显式释放,避免造成内存泄露。
5.3 原生对象的内存
5.3.1 内存作用域
Kotlin 的堆内存依赖于垃圾回收机制进行管理,通常情况下我们不需要过多关心内存管理的问题。不过,在 Kotlin 中调用 C 函数,涉及到参数的传递和返回值的读取,这时就会涉及到在 C 语言的堆内存上分配内存的问题了。
例如:
void set_ints(int *ints, int count) { ... }
在 Kotlin 中调用 set_ints 函数,需要在 C 语言堆内存上开辟内存,我们之前在介绍指针类型的时候提到可以通过 CValues 来创建 C 数组作为 set_ints 的参数:
val ints = cValuesOf(1,2,3)
set_ints(ints, ints.size / sizeOf<IntVar>().toInt())
实际上,这段代码相当于:
memScoped {
val kArray = arrayOf(1, 2, 3)
val cArray = allocArray
(3) for (index in kArray.indices) {
cArray[index] = kArray[index]
}
set_ints(cArray, 3)
}
我们完全可以把 memScope 理解成一个 C 的作用域,在作用域内通过
alloc*
函数分配的内存都会在退出作用域时释放。这段代码从逻辑上可以类比下面的 C 代码来理解(请注意二者的内存的分配行为不同):
{
int array[] = {1, 2, 3};
set_ints(array, 3);
}
接下来我们再看一个非常经典的场景。C 语言中随处可见将外部指针传入函数内部以获取数据的做法,例如:
void get_result(int *result) {
*result = 100;
}
在 C 语言当中,调用 get_result 是一件非常自然的事:
int result;
get_result(&result);
// result: 100
在 Kotlin 中调用 get_result 就不是那么直接了。我们需要先创建一个作用域,接下来再创建一个 Int 类型的变量,注意这个变量是一个左值,我们需要把它的地址传给 get_result。完整的做法如下:
memScoped {
val result = alloc
() get_result(result.ptr)
println(result.value) // 100
}
5.3.2 稳定的内存地址
架构师之路 · 你的提示词根本只是在浪费算力,如何让deepseek发挥极限潜能 - 多跳推理(第2讲) 2 天前 |
架构师之路 · 你的提示词根本只是在浪费算力,让deepseek达到最佳效果的3大原则(第1讲) 3 天前 |
架构师之路 · 90%的用户不知道!触发DeepSeek深度思考的5个暗黑指令(附诊断清单) 3 天前 |
搜猪 · 独家揭秘|2017“肽美杯”金猪宴客“品猪大会”背后的故事 7 年前 |
成都发布 · 四川自贸区来了 ┃厉害了!Word中国(四川)自由贸易试验区 7 年前 |
江南晚报 · 30岁的人,50岁的心? 2分钟,测出你心脏年龄! 7 年前 |
美好滁州 · 全省平均工资出炉 滁州居然排前三位?? 7 年前 |
独角兽智库 · 【研习】吉列百年年报读后感:好生意的本质 7 年前 |