
-
探索 Swift 和 Java 互操作性
了解如何在一个代码库中混合使用 Swift 和 Java。我们将介绍 swift-java 互操作性项目,该项目让你可以在 Java 程序中使用 Swift,反之亦然。我们将向你展示如何使用 swift-java 提供的工具和库来编写可在这两个运行时之间进行互操作的安全高效代码。
章节
- 0:00 - 简介与内容安排
- 2:41 - 运行时差异
- 3:31 - Java Native Methods
- 6:29 - SwiftJava
- 10:13 - 在 Swift 中调用 Java
- 14:01 - 在 Java 中调用 Swift
- 20:47 - 总结
资源
相关视频
WWDC25
WWDC23
-
搜索此视频…
大家好 我叫 Konrad 是 Swift 语言团队的一名工程师 今天 我想向大家介绍 我们在今年早些时候启动的 新互操作性方案: Swift-Java 互操作性 我们对互操作性充满期待 因为它让我们能够 在更多应用程序中使用 Swift 通过互操作性 我们可以循序渐进地将 Swift 引入用其他语言编写的现有代码库中 我们不需要进行大规模且风险高的 重写 可直接添加新功能 或使用 Swift 替换现有功能 无需改动现有的代码库 我们还可以重复利用 其他语言实现的现有库 同时也能让其他语言使用 在 Swift 中编写的库 我们实现互操作性的方式 不仅限于语言 因为我们集成了最适合 每个生态系统的构建工具 比如面向 C 或 C++ 的 CMake 或面向 Java 项目的 Gradle 自 Swift 问世以来 它就具备与 C 语言的高级互操作性 这种互操作性使 Swift 能够 无缝集成到 现有的 Apple 开发者生态系统中 并最终成为在这些平台上 进行开发的主要语言 两年前 我们推出了 C++ 互操作性方案 它支持在单个代码库中 无缝混合 C++ 和 Swift 库 这使得 Swift 能够在更多场景中 得以应用 要进一步了解相关信息 请观看 WWDC23 讲座 “融合 Swift 和 C++” 或今年的讲座“安全地混合使用 C、C++ 和 Swift” 在讨论互操作性时 我们可以关注两个方案 首先 我们可以主要用 Swift 编写应用程序 并调用一些用 Java 编写的代码 或者我们可以专注于让 Swift 能够 简单高效地从 Java 执行调用 Swift 的 Java 互操作性 支持这两个方案 我们在每个方案中 使用的工具和技术略有不同 但你可能会发现在一个项目中 也能同时使用所有这些内容 在我们开始讨论具体示例之前 让我们先弄明白 Java 和 Swift 运行时的差异 然后 我们将探讨应用 Java 互操作性的 三个实际示例 首先 通过使用 Java 现有的 原生方法功能 然后 我们将探讨如何让 Swift 可访问整个 Java 库 最后 我们将探索相反的方案 封装整个 Swift 库 以便 Java 项目轻松利用 我们先来比较一下这两种运行时 毕竟 Java 运行时与 Swift 现有互操作的 其他原生语言存在显著差异 不过 如果从宏观视角来看 或许可以说 Swift 可能是目前 与 Java 最相似的原生语言 这两种语言都提供 具有相似继承模型的类 并且都提供一种自动内存管理方法 对开发者来说基本上是透明的 它们都提供非常相似的泛型系统 尽管在运行时表示方式不同 最后 Swift 错误与 Java 异常一样都可以抛出 虽然 它们在运行时携带的信息量 略有不同 重要的是 通过精心设计与这些功能的交互方案 我们能够实现两种语言间 大多数 API 的表达 现在我们已经了解了这些语言的 差异及相似之处 让我们一起来看看如何扩展 现有的 Java 应用程序 我们将从基础知识开始 先专注于在 Swift 中 实现单个函数 为了实现这个操作 我们将使用 Java 语言功能 名为 Native Methods Java Native Methods 属于 Java Native Interface API 简称 JNI 自 1997 年以来 也就是 iPod 出现之前 JNI 一直属于 Java 它专门用于实现 Java 与原生代码的互操作性 它通常用于以下场景:通过将任务 移出 Java 堆以实现高性能目标; 或者使用 无 Java 等效的原生库 它仍然是 Java 和本机代码 进行互操作的主要方式之一 并且这些 API 从那时起 几乎没有改变过 因此 我们只有开始探索 JNI 才有意义 为了稍微熟悉一下 JNI 让我们按照常规步骤使用 JNI 不借助任何支持库或工具 在 Java 应用程序端 你将定义一个原生函数 这个函数将使用原生代码实现 为了说明 JNI 如何 将对象传递给原生代码 我们使用大写 Integer 类型 作为参数 并返回这个函数的类型 与原始 Int 值不同 这些是 Java 对象 我们必须相应地使用它们 然后 必须将带 -h 标志的 Java 编译器 用于包含原生方法的任何文件上 这将生成一个包含 C 函数声明的 C 标头文件 当调用相应 Java 原生方法时 JVM 将尝试调用这些函数 这个声明有一个相当冗长的名称: Java_com_example_JNIExample_compute 它会匹配原生方法的软件包、 类和方法名 它会接受名为 JNI 环境的参数 以及用于 Java 对象调用方法的 This 引用的 jobject 参数 还有原始 Java 函数声明的所有参数 最后 你需要使用选择的原生语言 也就是 Swift 来实际实现这个函数
代码还真不少呢! 尽管样板代码确实掩盖了 实际的逻辑实现 就是这个部分 但它并非只是噪音 它们是无数个可能导致细微错误的 潜在风险点 任何失误都可能引发致命崩溃 综上所述 JNI 虽是 JVM 调用原生代码的主流方案 但正确使用难度极高 要提高方法实现的性能 我们可能需要引入缓存 等优化技术 但这会进一步 增加代码复杂度 还需要考虑其他构建步骤 和 C 标头等问题 手动匹配所有方法签名和魔术字符串 非常容易出错 我们还必须谨慎地管理任何已创建 或接收的 Java 对象的生命周期 因此 在没有额外支持的情况下 使用 JNI 开发 虽然可行 但开发体验非常低效 SwiftJava 应运而生 它是 Swift 和 Java 互操作性的新解决方案 我们构建的 SwiftJava 将为 Swift 和 Java 开发者提供 一种灵活、安全且高效的 双语言交互方案
SwiftJava 由几个部分组成 你可以单独或搭配使用 首先是 Swift 软件包 它提供 JavaKit 库和宏 这使得处理 JNI 代码变得更安全 就像在之前的示例中 展示的那样 然后 我们有一个 Java 库 名为 SwiftKit 它可以帮助 Java 应用程序 有效处理 Swift 对象 最后是 Swift-Java 命令行工具 以及其他构建系统集成 比如 SwiftPM 插件 甚至是那些正在开发中、 与 Gradle 等主流 Java 构建工具 深度集成的方案 让我们重新执行刚才的示例 但这次 我们将使用 SwiftJava 提供的工具 我们不使用 Java 编译器 而是应用 Swift-Java 命令行工具 生成必要的桥接 我们需要提供模块名称 其中应该写入生成的源代码 以及一些额外的配置 在更复杂的项目中 这将由 SwiftPM 中的构建插件 或者你正在使用的其他构建系统触发 这个工作流的结果是 包含装饰性元素的 Swift 文件 这些元素会描述导入的 Java 类 如果 Java 类型包含成员方法 它们也会出现在相应的 Swift 类型上 这意味着我们可以从 Swift 端 回调这些 Java 函数 最后 生成的 JNIExampleNativeMethods 协议 包含我们可为这个类型实现的 所有原生方法 这可以替代我们之前使用的 C 标头 要实际实现原生计算函数 我们只需在生成的 JNIExample 类上编写扩展 并使它符合 JNIExampleNativeMethods 协议 我们还要使用由 JavaKit 提供的 JavaImplementation 宏 对它进行注释 接下来 编译器将帮助我们实现 正确的必需函数签名 唯一需要额外注意的是 必须使用 JavaMethod 宏进行注释 这个宏 会处理一些复杂的 JNI 细节 现在 借助 SwiftJava 我们的方法实现变得更加简单 我们只需专注于业务逻辑 更棒的是 由于处于 Swift 中 我们可使用任何 Swift 库 例如 我可能需要调用 某些加密算法的 原生实现 Swift 生态系统正好有适合的库 我可以导入 Crypto 模块 例如 计算传入数据的 SHA256 哈希值 你可能还记得我们之前 JNI 实现中的种种问题 其实这些问题大多 并不在于 JNI 本身 而在于正确使用的高难度 得益于 SwiftJava 对 JNI 的方法 我们不仅能规避大量模板代码 默认生成的代码也更易维护且更安全 我们完全不需要与 C 标头进行交互 相反 我们可以依赖 类型完备、自动生成的函数签名 这为我们避免了可能需要 花费很长时间进行调试的错误 最后 相比手动编写语言桥接代码 对象生命周期管理有了显著改进 这对编写跨语言内存安全代码 至关重要 总而言之 相比手动编写 使用 SwiftJava 执行 JNI 交互 会更便捷且更安全 以上内容只是冰山一角 SwiftJava 还会为你带来更多惊喜 接下来 我们来设想一种情况 我们需要在 Swift 中调用 Java 库 我们将再次使用相同的 Swift-Java 工具 但这一次 让我们重点关注 如何导入现有的整个 Java 库 而非单一的类型 例如 假设我想调用 Swift 中常用的 Apache commons-csv Java 库 我需要同时找到这个库 以及它的所有下游依赖项 依赖项解析的复杂度会迅速攀升 因为 Java 库往往存在大量 传递依赖项 而它们也会有自己的依赖项 情况愈加复杂 幸好 SwiftJava 可解决这个问题 我们只需准备要使用的库的 工件坐标 通常 你可通过在线搜索 轻松找到它们 如果你自己无法确定 可以询问团队中的 Java 开发者 依赖项将表示为三元组: 标识特定工件的 Artifact ID、 标识发布库的组织的 Group ID 以及版本号 接下来 我们将利用 SwiftJava 的 Gradle 集成 这是 Java 生态系统中常用的 构建工具和 依赖项管理器 为了向 Gradle 声明这种依赖项 只需将这三个值用冒号分隔开 这样 就能以 Gradle 可理解的格式 形成依赖项描述符 现在我们了解了依赖项坐标 我们可通过两种方式下载依赖项 并将它们封装到 到 JavaApacheCommonsCSV 目标中 第一种方法是使用 SwiftJava 构建工具插件 在目标的 swift-java.config 文件中 我们添加了依赖项部分 其中列出我们要解析的所有根依赖项 这非常方便 现在当我们构建 SwiftPM 项目时 插件将自动调用 Gradle 以解析 Java 依赖项 但是 SwiftPM 会强制执行安全沙盒 这可确保构建的插件 无法访问任意文件和网络 因此 如果你想使用这种方法 可以选择在生成项目时禁用安全沙盒 这可能并非在所有环境中都可行 因此 你可采取一种替代方法 我们可以转而使用 Swift-Java 命令行工具的 Resolve 命令 通过向它传递包含配置文件的 模块名称 这个工具将解析依赖项 并将生成的类路径写入文件 由于这是在 SwiftPM 构建之外 执行的操作 我们不必禁用沙盒 不过 这意味着 在构建项目之前 你需要手动触发这类依赖项解析 你需要根据工作流程进行取舍 选择更适合自己的模型
无论如何选择 现在我们都可 在 Swift 中调用 Java 库 只需导入 JavaKit 和 JavaApacheCommonsCSV 模块 然后 在 Swift 进程中启动 JVM 以运行 Java 代码 这样 我们就可在 Swift 应用程序中 使用 Java 了 比如这里 我们使用的是 JDK 的 FileReader 并将它传递给我们刚导入的 CSV 库 更重要的是 我们甚至可以 直接在返回的 Java 集合上 使用 Swift 的 for-each 循环
了解了 SwiftJava 如何 同时使用 Gradle 和 SwiftPM 来提供出色的用户体验 我们就能够导入整个 Java 库 而无需修改任何 Java 源代码 生成的 Swift 源代码会利用 JavaKit 提供的 JDK 包装器类型 并无缝处理用户定义的类型 JavaKit 还会简化 Java 对象的 生命周期管理 这可通过在必要时将对 Java 对象 的引用提升为全局引用来实现 我们今天要讨论的最后一项技术 是将整个 Swift 库 提供给 Java 应用程序调用 这是非常重要的用例 因为我们可在 Swift 中实现 关键的核心业务逻辑 并将它用于我们的 所有应用程序和服务中 而不用考虑他们是否采用了 Swift
我们之前提到过 互操作性必须是双向的 我们应当深刻理解: 优化 Java 到 Swift 的互操作体验 不仅能推动更多项目采用 Swift 还能提升开发者的效率 这正是将新语言引入代码库 对社会产生的深远影响 现在 如果我们要使用以前的技术 公开整个 Swift 库 我们必须在 Java 端编写 大量包装器函数 这仅适用于少量函数 如果涉及整个库 就应该考虑另一种方法 这与向 Swift 公开 Java 库的 操作类似 现在 我们要让 Java 调用 Swift 变得更加顺畅和简单 要实现这一目标 需要通过 Java 类 封装 Swift 库的所有类型 并将它打包为完整的 Java 库 进行分发 这一次 我们会彻底弃用 JNI 转而使用新的 Foreign Function 和 Memory API 这些 API 已于去年 3 月稳定 并随 Java 22 正式推出 它们增强了对原生内存 和原生调用方式的控制 在某些情况下 可以替代 JNI 通过使用这些新 API 我们能够在 Java 和 Swift 运行时 以及内存管理之间构建深度集成 这在之前根本无法实现 这提升了 Java 调用原生代码时的 安全性和性能 在本例中 我们使用 Swift 结构体类型 这个类型表示我想向 Java 公开的 某个业务对象 它是一种值类型 因此没有稳定的对象标识 并非 Java 对象可以表达的 因此 我们必须谨慎地 处理 Java 中的这个对象 它还具有公共属性、构造器 以及一些操作的方法 为了向 Java 公开这个类型 我们将再次使用 Swift-Java 命令行工具 但是 这一次 我们的使用模式 略有不同 我们将为它提供 Swift 输入路径 和输出目录 用于生成的 Swift 和 Java 源代码 然后 这个工具将获取 输入路径中的所有源代码 并生成 Java 类 这些类充当 Swift 类型 和函数的访问器 我们还将生成一些必要的 Swift 辅助代码 最后 所有内容 (包括构建为动态库的 Swift 代码) 都将被编译并打包到 Java 库中 生成的 Java 类将如下所示 它实现了 Swift 值接口 因为它是一个结构体 这个类包含 Self MemorySegment 这实际上是指向 原生内存中实例的指针 它还表示所有公共构造器、属性 以及使用等效 Java 签名的函数 在这些代码中 我们有源代码生成的高效代码 可使用 Foreign Function API 执行原生调用 现在 在 Java 应用程序中 我们可以依赖生成的 Java 源代码 要创建和管理原生 Swift 值 我们需要 SwiftArena 它负责 Swift 对象的 内存分配和生命周期 准备好 Arena 后 我们只需调用 Swift 值的构造函数 就好像它是普通的 Java 类一样 让我们借这个机会进一步讨论 这里的原生和 Java 内存资源 是如何进行管理的 首先 会在 Java 堆上分配 新的 Java 包装器对象 这个对象由 JVM 的垃圾回收器管理 然后 源代码生成的构造函数 会使用 SwiftArena 中传入的数据 在原生堆上分配并初始化 Swift 值类型的实例 通常 像 SwiftyBusiness 结构体 这样的值类型 都会在栈上进行分配 但由于我们需要稳定的内存地址 我们会在堆上分配它 这样我们就能从 Java 包装器对象 安全指向这个内存地址 最终 当 Java 包装器不再使用时 垃圾回收器将采取回收和销毁操作 这也将触发对 Swift 端 原生实例的销毁 因此 从内存管理的角度来看 这是安全的 然而 与 Swift 不同的是 依赖这种对象终结 会给垃圾回收器带来较大的压力 因为它需要对这类对象 执行额外的追踪 这还会导致原生 Swift 值的 去初始化时机变得不可预测 因此 虽然这个模式易于上手 但还有管理原生内存的更好方法 我们可以改用 try-with-resources Java 语法 结合 Confined Arena 类型 来替代 Auto Arena 在这里 对象分配将以相同方式执行 但是 try-with-resources 会更改对象的销毁方式 具体来说 在 triscope 结束时 Arena 将关闭 这会触发对 Java 包装器对象的销毁 进而触发原生堆上 Swift 值的销毁 这种方法好得多 垃圾回收器不必承担对象终结的工作 因为当这类操作大规模执行时 可能会引发问题 并且我们还重新获得了 对象去初始化过程定义清晰、 有序进行的特性 这正是众多 Swift 程序依赖的 因此 请尽量使用 Scoped Arena 而不是依赖垃圾回收器 来获得最佳应用程序行为和性能 让我们回顾一下今天的内容 我们能够封装完整的 Swift 库 只需调用一次 Swift-Java 命令行工具即可 我们能够将这样的 Swift 代码 封装成 Java 库 并进行发布 这样便能 在 Java 项目中轻松调用这些库 进一步简化了团队 对 Swift 的采用过程 通过使用新的 Foreign Function 和 Memory API 我们能够严格控制 对象的分配和生命周期 甚至是 Swift 值类型 今天 我们介绍了许多 使用 Swift 和 Java 的不同技术 你可单独或搭配应用 具体取决于你的项目需求 尽管我们才刚刚起步 还有很多地方需要打磨和完善 但 SwiftJava 提供了 非常不错的解决方案 可实现这两种语言之间的互操作性 通过使用 SwiftKit 和 JavaKit 支持库 你可编写安全且高效的代码 在一种语言中调用另一种语言 JavaKit 宏以及 Swift-Java 命令行工具 会自动生成原本难以维护的 各种样板代码
最后 诚挚邀请大家加入我们 共同参与这个项目的开发 这个项目完全开源 并可通过 Swiftlang Github 组织获取 我们仍面临许多值得探索的 技术挑战与创新方向 如果你还没准备好参与开发工作 但想进一步了解 Swift 和 Java 或者分享你的想法和反馈 不妨加入我们的 Swift 论坛 非常感谢大家观看! 至于我嘛 该去给自己倒杯咖啡了
-
-
9:05 - Implement JNI native methods in Swift
import JavaKit import JavaRuntime import Crypto @JavaImplementation("com.example.JNIExample") extension JNIExample: JNIExampleNativeMethods { @JavaMethod func compute(_ a: JavaInteger?, _ b: JavaInteger?) -> [UInt8] { guard let a else { fatalError("Expected non-null parameter 'a'") } guard let a else { fatalError("Expected non-null parameter 'b'") } let digest = SHA256Digest([a.intValue(), b.intValue()]) // convenience init defined elsewhere return digest.toArray() } }
-
12:30 - Resolve Java dependencies with swift-java
swift-java resolve --module-name JavaApacheCommonsCSV
-
13:05 - Use a Java library from Swift
import JavaKit import JavaKitIO import JavaApacheCommonsCSV let jvm = try JavaVirtualMachine.shared() let reader = FileReader("sample.csv") // java.io.StringReader for record in try JavaClass<CSVFormat>().RFC4180.parse(reader)!.getRecords()! { for field in record.toList()! { // Field: hello print("Field: \(field)") // Field: example } // Field: csv } print("Done.")
-
16:22 - Wrap Swift types for Java
swift-java --input-swift Sources/SwiftyBusiness \ --java-package com.example.business \ --output-swift .build/.../outputs/SwiftyBusiness \ --output-java .build/.../outputs/Java ...
-
18:55 - Create Swift objects from Java
try (var arena = SwiftArena.ofConfined()) { var business = new SwiftyBusiness(..., arena); }
-
-
- 0:00 - 简介与内容安排
了解一个名为“swift-java”的实验性 Swift 语言库,它基于与 Objective-C、C 和 C++ 的现有互操作性功能,使 Swift 能与 Java 无缝协作。借助这种互操作性,你可以逐步将 Swift 引入现有的 Java 代码库,跨语言重复使用资源库,以及将 Swift 资源库与 Java 项目集成。 “swift-java”提供的互操作性支持从 Swift 调用 Java 代码,反之亦然。这个团队正在开发工具和技术来处理 Java 和 Swift 运行时之间的差异,并为转换为 Swift 类型的 Java 代码提供更出色的内存安全性。
- 2:41 - 运行时差异
Swift 和 Java 具有一些共同的特性 (例如类、继承、自动内存管理、泛型和错误处理),但也存在运行时差异。尽管如此,但这两种语言的相似之处使得其中一种语言的大多数 API 都可以用另一种语言表达。
- 3:31 - Java Native Methods
Java Native Interface API (JNI) 于 1997 年初推出,它使 Java 虚拟机 (JVM) 中运行的 Java 代码能够与原生代码 (例如 Swift) 进行互操作。这通常是为了提高性能或利用没有 Java 等效项的原生库。 若要使用 JNI,需要使用 Java 语言定义一个“原生”函数,并生成相应的 C 语言头文件。这个头文件包含一个必须使用原生语言 (如 Swift) 实现的 C 函数声明。这一过程涉及管理对象生命周期、匹配方法签名以及处理冗长的样板代码 (这些代码容易出错且耗时,可能导致严重的崩溃)。
- 6:29 - SwiftJava
SwiftJava 增强了 Swift 语言与 Java 语言之间的互操作性。它提供了一套工具,包括 Swift 和 Java 资源库、命令行工具以及构建系统集成,可简化和保护这两种语言之间的交互。 “swift-java”命令行工具可自动生成桥接代码,无需手动进行 C 语言头交互。这使得代码更简洁、更易于维护,同时也改进了对象生命周期管理和类型安全性。现在,你可以专注于业务逻辑,这得益于两个方面:一方面可以充分利用 Swift 和 Java 生态系统的强大功能,另一方面也可避免手动 JNI 实现中常见的陷阱和错误。
- 10:13 - 在 Swift 中调用 Java
SwiftJava 支持将 Java 资源库集成到 Swift 项目中。若要导入整个 Java 资源库 (例如 Apache Commons CSV),请准备以下工件坐标:“groupId”、“artifactId”和“version”。 然后,SwiftJava 会利用开源构建系统 Gradle 来解析 Java 依赖项。下载和封装 Java 依赖项的方法有两种: 使用 SwiftJava 构建工具插件,这需要禁用 SwiftPM 的安全沙盒;或者使用“swift-java”命令行工具的“resolve”命令,这条命令会在构建过程之外执行解析。 解析完 Java 依赖项后,你可以将 Java 资源库导入 Swift,在 Swift 进程中启动 JVM,并无缝地使用 Java 代码和集合以及 Swift 功能 (借由 JavaKit 处理对象生命周期管理)。
- 14:01 - 在 Java 中调用 Swift
SwiftJava 支持将 Swift 资源库集成到 Java 项目中。为了实现这一点,本次 WWDC25 讲座介绍了一种新方法,无需在 Java 端使用大量封装容器函数。整个 Swift 资源库都使用 Java 22 中推出的全新 Foreign Function and Memory API (FFI),通过 Java 类进行封装。 FFI 改进了对原生内存的控制,并支持 Java 运行时和 Swift 运行时以及内存管理系统之间的深度集成。通过使用 FFI,从 Java 调用 Swift 代码的过程变得更加高效和安全。 “swift-java”命令行工具用于生成必要的 Java 类和 Swift 帮助程序代码。这一工具可自动生成样板,使这个过程更加简单。生成的 Java 类将充当 Swift 类型和函数的访问器,有效地将 Swift 资源库的功能暴露给 Java。 在 Java 中使用原生 Swift 对象时,适当的内存管理至关重要。本次讨论重点介绍了两种方法:使用依赖于 Java 垃圾回收器 (GC) 的“AutoArena”,以及使用“try-with-resources”和“ConfinedArena”。后一种方法是首选方法,可确保对象去初始化过程定义明确且有序进行,从而避免性能问题和增加 GC 的负担。 借助这一技术,你可以构建和发布 Swift 封装的 Java 资源库,使它易于在 Java 项目中使用。这样就简化了拥有扎实 Java 基础的团队采用 Swift 的过程,可营造更灵活、更高效的开发环境。SwiftJava 的后续开发目标是,进一步完善和改进这些技术,为实现两种语言之间的互操作性提供强大的解决方案。
- 20:47 - 总结
Swift 编程语言是开源的,托管在 GitHub 上,SwiftJava 等资源库也同样如此。广大开发者在 Swift 论坛上非常活跃,你可以在这些论坛中学习、分享想法和提供反馈。