Kotlin E03:Native 跨过 C 指针边界要付出什么
这里以本地合成输入验证 Linux x86_64。Kotlin/Native 编译器、Gradle 插件均固定为 2.2.21,使用原 Wrapper 8.14、JDK 21 和宿主 GCC 13.3.0。没有 macOS、Xcode 或 Swift 编译器;Swift/Objective-C 的运行结果标 NOT_RUN,不能凭 Linux C 绑定推出 Apple 目标已验收。
公共规则先要由 Native 自己跑一次
shared-model 沿用 E02 的 commonMain/TaskState.kt,新增 linuxX64 目标。相同的 commonTest 在这个目标下再次检查 Queued + Start → Running、成功终态、排队取消,以及“排队时直接 Success 必须抛错”。测试源集的 actual fun targetName() 返回 Linux/Native,生成 XML 报 tests="1"、failures="0":
1 | |
这行出自 linuxX64Test,不是把 jvmTest 重命名。首次构建从 Kotlin 工具链获取 Native 2.2.21、LLVM 与 sysroot 等依赖,实际测试和下载日志见 writing-plans/kotlin/verification/E03/native-target.txt。它只证明公共状态机的这一组断言在本地 Native 测试进程成立;35的 JDBC 连接、38的 Netty 进程仍是 JVM 路径,没被搬到 Linux Native。
同是指针形参,生成绑定未必长得一样
本地 C 文件只暴露两个签名:
1 | |
GCC 将该文件编成静态库,Kotlin Gradle 的 cinterop 任务读取头文件与 .def,最后链接到 Linux x86_64 ELF。task_utf8_bytes 遍历零终止 C 字符串,返回非零字节数;task_bytes_checksum 遍历显式长度的字节数组,并对负长度返回 -1。两者都不保留传入地址,也不读取真实用户数据。C 文件、头文件、互操作定义和构建脚本随源码归档提供,不要求在目标机器上连某个生产系统。
最初按“C 参数是指针”推测,把 "hé".cstr.ptr 传给 task_utf8_bytes,Native 编译器拒绝:实际生成形参在本版本是 String?,而传入类型是 CPointer<ByteVarOf<Byte>>。保留了这次非零构建和原始诊断 interop-initial-failure.txt。改用 task_utf8_bytes("hé") 后,Native 绑定执行字符串转换,C 返回 3;Kotlin 的 "hé".length 是 2。C 函数测的是转换后的 UTF-8 字节,不是 Kotlin 字符数,也不是 JVM 的 UTF-16 code unit 约定。官方 C interop 文档描述了声明映射和内存作用域,具体生成形参仍应按本机编译输出核对。
另一个参数是 const uint8_t*,不能把 Kotlin ByteArray 当 String 交给编译器隐式转换。程序用 usePinned 固定 byteArrayOf(1, 2, 3, 4),在作用域内取 addressOf(0) 并传给同步 C 函数。C 求和返回 10;同一数组另以长度 -1 传入时返回 -1,证实边界代码先拒绝非法长度而不是走入数组读取。离开 usePinned 后不再使用该地址;C 若储存指针以供异步任务稍后读取,这段代码和当前测试都不能为它的生命周期背书。usePinned 不是随意关闭 Native GC,它只为借用地址的时段建立约束。
复跑输出:
1 | |
这一行在 interop-success.txt 中与 BUILD SUCCESSFUL、进程退出 0 同时保存。编译产物的 ELF64 头信息见 verification/E03/environment.txt;看见 cinterop 生成文件或一个 .klib 并不足以声称目标程序已执行。
Worker 的完成点,不是 JVM 线程模型的复刻
Native 程序启动一个 Worker,把不可变的 TaskState.Queued 作为输入交给它,在工作函数里计算 advanceTask(state, TaskSignal.Start)。主调用方等待 Future.result 再请求 Worker 终止,因此输出中的 Running 已越过一次 Native 工作单元的完成点。这是一次受控传递,没有让两个 Worker 同时写共享任务状态,也没有建立吞吐或竞态测试。
Kotlin 官方的 Native 内存管理文档说明对象可被不同线程访问及跟踪式 GC 的工作方式;可访问不等于可无同步写入。29里讲的并发边界也不能由一个 Worker.result 自动证明。Native 的 Worker API、GC 和 Kotlin/JVM 的 Thread、虚拟线程、G1 等不是同一套运行环境。当前 Worker 示例只作为固定版本的最小可运行边界,不推广其调度时延或未来 API 稳定性。
在什么地方停止推论
复跑需要 Linux x86_64、JDK 21、GCC 和可用的 Kotlin/Native 2.2.21 工具链:
1 | |
脚本先以 gcc/ar 构造本地静态库,再用 Wrapper 跑 linuxX64Test linkDebugExecutableLinuxX64,最后执行真正的 *.kexe;源码归档 SHA 与解包复跑见素材和验证目录。原 JVM/JS 两目标回归须另跑,不能因为第三个目标通过就跳过旧断言。
Swift 的对象桥接、ARC 所有权与 suspend 暴露需要 Apple 目标和实际 Swift 消费者;官方 Swift/Objective-C 互操作文档只能用作资料入口。当前 Linux 环境没有 Apple SDK,相关构建、动态释放、异常映射与 Swift 调用全部 NOT_RUN。C 指针即使经 Kotlin/Native 转换,也只在这个同步调用边界得到实验支持;JVM 的 GC、线程数量与 JDBC 取消结论均未迁移。
练习与答案
练习一:如果 C 函数保存 usePinned 中取得的地址,等 Kotlin 作用域结束后再读,当前实验能否为它背书?答案:不能。usePinned 的约束只覆盖同步调用期间,异步保存指针需要新的所有权协议和测试。
练习二:task_utf8_bytes("hé") 返回 3,能否说明 Kotlin String.length 在 Native 上也按 UTF-8 字节计?答案:不能。返回 3 的是 C 函数接收转换后的零终止 UTF-8 字符串;Kotlin 字符串长度仍不是数据库或网络字节数。
练习三:一次 Worker.result 返回 Running,能否证明 Native 共享状态可无锁并发写?答案:不能。当前只传递不可变输入并等待一个 Future,没有同时写同一个任务状态。






