大多数浏览器和
Developer App 均支持流媒体播放。
-
探索 Swift 和 Java 互操作性
了解如何在一个代码库中混合使用 Swift 和 Java。我们将介绍 swift-java 互操作性项目,该项目让你可以在 Java 程序中使用 Swift,反之亦然。我们将向你展示如何使用 swift-java 提供的工具和库来编写可在这两个运行时之间进行互操作的安全高效代码。
章节
- 0:00 - Introduction & Agenda
- 2:41 - Runtime differences
- 3:31 - Java Native methods
- 6:29 - SwiftJava
- 10:13 - Call Java from Swift
- 14:01 - Call Swift from Java
- 20:47 - Wrap up
资源
相关视频
WWDC25
WWDC23
-
搜索此视频…
Hello, my name is Konrad. I’m an engineer on the Swift language team. And today, I’d like to introduce you to a new interoperability effort that we’ve kick-started earlier this year: Swift-Java Interoperability.
We’re excited about interoperability because it broadens the range of applications where we can use Swift. Using interop, we can incrementally introduce Swift to existing codebases written in other languages. We don't have to do big, risky rewrites, and instead we can just add a new feature or replace an existing feature with Swift without touching the rest of the existing codebase. We can also reuse existing libraries implemented in other languages. We can make libraries written in Swift available to them instead. And the way we approach interoperability goes beyond just the language as well, as we integrate with build tools that are most appropriate for each ecosystem. like CMake for C or C++ or Gradle for Java projects.
Interoperability with C languages has been a first-class feature of Swift since day one. It enabled Swift to seamlessly integrate with the existing Apple developer ecosystem and eventually become the primary language for developing on these platforms. Two years ago, we introduced the C++ interoperability effort, which enabled seamless blending of C++ and Swift libraries in a single code base. This opened up many possibilities for using Swift in even more places. To learn more about this, you can check the “Mixed Swift and C++ session” from WWDC 2023, or this year’s “Safely Mixed C, C++ and Swift session”.
When we talk about interoperability, there are two directions we can focus on. First, we can be writing an application primarily in Swift and call into some code written in Java. Or we can focus on making Swift easy and efficient to call from Java instead. Swift’s Java interoperability supports both these directions. The tools and techniques we use in each direction are slightly different, but you may find yourself using all of them together in a single project.
Before we jump into specific examples, let’s revise the differences between the Java and Swift runtimes. Then we’ll explore three practical examples of putting Java Interoperability to work. First, by using Java’s existing native methods feature. Then we’ll explore how we can make an entire Java library accessible to Swift. And finally, we’ll explore the opposite direction and wrap an entire Swift library for easy consumption by a Java project. Let’s start with comparing the two runtimes. After all, the Java runtime is very different than any other native language Swift already has interoperability with.
Although, if we zoom out a bit, we could actually say that Swift might be the most similar native language to Java out there. Both languages offer classes with a similar inheritance model. Both provide a way of automatic memory management, largely transparent to the developer. They both offer a generic system that’s pretty similar as well, although represented differently at runtime. And finally, Swift errors can be thrown, just like Java exceptions. Although, again, the amount of information they carry at runtime is slightly different.
The important thing is that by carefully choosing how we’re going to interact with those features, we’re actually able to express most APIs from one language in the other.
Now that we’ve reminded ourselves of the differences and similarities of these languages, let’s jump right in and see how we can extend an existing Java application. We’ll start really simple and just focus on implementing a single function in Swift. In order to do this, we’ll use the Java language feature called Native Methods.
Java Native Methods are part of the Java Native Interface API, also known as JNI for short. JNI has been part of Java since 1997, that’s before the iPod, and it’s been designed as a way for Java to interoperate with native code. It is often used in order to achieve performance goals, by moving work off the Java heap, or in order to use native libraries which don’t have a Java equivalent. It is still one of the primary ways Java and native code interoperate, and the APIs have not really changed ever since. So it only makes sense for us to start our exploration with JNI itself.
In order to familiarize ourselves a little bit with JNI, let’s follow the traditional steps one would have to follow to use JNI without any support libraries or tools. On the Java application side, you’d define a native function, which will implement using native code. In order to illustrate how JNI deals with passing objects to native code, we’re using the Uppercase Integer types as arguments and return type of this function.
Unlike primitive Int values, these are Java objects and we’ll have to work with them accordingly. Then you’d have to use the Java compiler with a “-h” flag on any files that contain native methods. This will result in a C-header file that contains C function declarations that the JVM will attempt to invoke when the corresponding Java native method is called. The declaration has a pretty verbose name: Java_com_example_JNIExample_compute, which matches the package, class, and method name of the native method. It accepts something called the JNI environment, and jobject parameter for the This reference of the Java object we're calling the method on, and all the parameters of the original Java function declaration. Finally, you’d actually implement this function in the native language of your choice, which for us would be Swift.
Well, that's a lot of code, isn't it? Although boilerplate is really drowning out for actual logic implementation, which is over here. It isn't just noise. It is numerous opportunities to get something slightly wrong, which would then result in a fatal crash.
In summary, JNI is a well-supported way to call native code from the JVM, but it’s also very difficult to use correctly.
To make method implementations performant, we may need to involve caching and other techniques which further complicate the code. There’s additional build steps and C headers to worry about. Manually matching all the method signatures and magic strings is really error-prone.
And we also have to carefully manage object lifetime of any created or received Java object.
So doing JNI without additional support is possible, but it’s not a great experience.
Which brings us to SwiftJava, the new home of the Swift and Java interoperability effort. We’re building SwiftJava to provide Swift and Java developers a flexible, safe and performant way to interact between those two languages.
SwiftJava consists of a few pieces that you may reach for independently or together. First, a Swift package, which provides the JavaKit library and macros, which make dealing with JNI code, like we’ve shown in the previous example, much safer. Next, we have a Java library called SwiftKit. It helps Java applications deal effectively with Swift objects. And finally, the Swift-Java command line tool and other build system integrations, like for example, the SwiftPM plugin, or even the work in progress ways to integrate with popular Java build tools such as Gradle.
Let’s redo that last example, but this time, we’ll use the tools provided by SwiftJava. Instead of a Java compiler, we’ll use the Swift-Java command line tool to generate the necessary bridging. We need to provide it a module name where the generated sources should be written and some additional configuration. In more complex projects, this would be triggered by a build plugin in SwiftPM or a different build system you're using. The result of this workflow is a Swift file containing decorations that describe the imported Java class. If the Java type had any member methods, they would appear on the corresponding Swift type as well, allowing us to call back into these Java functions from Swift. And finally, the generated JNIExampleNativeMethods protocol contains all the native methods that we can implement for this type. This serves as a replacement for the C header we were using before. To actually implement your native compute function, all we need to do is write an extension on the generated JNIExample class and conform it to the JNIExampleNativeMethods protocol. We also have to annotate it using the JavaImplementation macro which is provided by JavaKit. Next the compiler will assist us in implementing the correct required function signature. The only additional thing to remember is that we need to annotate using the JavaMethod macro, which handles some further JNI details for us.
Thanks to SwiftJava, our method implementation became much simpler now, and we can just focus on our business logic. What’s even better though, is that now since we’re in Swift, we can use any Swift library we’d like. For example, I might want to use a native implementation of some cryptographic algorithm. The Swift ecosystem happens to have a great library for that, and I can import the crypto module and, for example, compute the SHA256 hash for the passed-in data. You may remember the list of issues we had in our previous JNI implementation. Most of them are not as much about JNI, but about how difficult it was to use correctly.
Thanks to SwiftJava’s approach to JNI, we can avoid a lot of the boilerplate and the resulting code is much easier to maintain and save by default. We don’t need to interact with C headers at all, and instead we can rely on well-typed generated function signatures.
This saves us from mistakes, which can take a long time to debug. And finally, object lifetime management is much improved in comparison to manually writing the glue between the languages by hand. This is crucial for writing memory safe code crossing language barriers. So overall it is much nicer and safer to do any kind of JNI interactions with SwiftJava rather than writing it by hand.
But we’re just getting started, and SwiftJava has lots more to offer. Next let’s consider a situation where we’d like to use a Java library from Swift. We’ll be using the same Swift-Java tool again, but this time let’s focus on how we’d approach importing a whole existing Java library, not just a single type. For example, let’s say I’d like to use the popular Apache Commons CSV Java library from Swift. Not only do I need to find the library itself, but also all of its downstream dependencies. Dependency resolution gets complicated very quickly, since Java libraries often have many transitive dependencies, which also have their own dependencies, and so on. Thankfully, SwiftJava can take care of this for us. All we need to do is prepare the artifact coordinates of the library we'd like to use. You can usually find them easily by searching online, you can ask a Java developer friend on your team if you're not sure yourself. The dependency will be expressed as a triple of: an Artifact ID identifying the specific artifact, a Group ID identifying the organization publishing the library, and a version number. Next we’ll make use of SwiftJava’s Gradle integration, which is a popular build tool and dependency manager in the Java ecosystem. In order to express this dependency to Gradle, all we need to do is put columns between these three values. In this way we’ve just formed a dependency descriptor in a format Gradle understands. Now that we know the dependency coordinates, we have two options to choose from for how to download and wrap them in our JavaApacheCommonsCSV target. The first method is to use the SwiftJava build tool plugin. Inside our target’s swift-java.config file we’d add a dependencies section listing all the root dependencies we’d like to resolve. This is quite nice. Now the plugin will automatically invoke Gradle to resolve Java dependencies whenever we build our SwiftPM project. However, SwiftPM enforces a security sandbox that makes sure built plugins cannot access arbitrary files and network. So if you’d like to use this approach, you can choose to disable the security sandbox when building the project. This may not be viable in all environments, so here’s an alternative approach you can take. Instead, we can use the Swift-Java command line tool’s, Resolve command. By passing it a module name that contains the config file, the tool will resolve the dependencies and write the resulting class path to a file. Since this is performed outside of the SwiftPM build, we don’t have to disable the sandbox.
It does mean, though, that you would have to manually trigger the dependency resolution like this before building your project. This is a trade-off and you can choose whichever model better suits your workflow.
Either way, now we’re able to use the Java library from Swift. All we need to do is import JavaKit and the JavaApacheCommonsCSV modules.
Then we start the JVM inside our Swift process in order to run the Java code. With that, we’re ready to use Java from inside our Swift application. Like here, we’re using the JDK’s FileReader and passing it to the CSV library that we just imported. What’s more, we can even use Swift’s for-each loops directly on returned Java collections.
We’ve learned how SwiftJava makes use of both Gradle and SwiftPM to provide a great user experience. We’re able to import entire Java libraries without having to modify any of the Java sources. The source generated Swift code makes use of JavaKit provided JDK wrapper types and handles user-defined types seamlessly. JavaKit also simplifies lifetime management of Java objects by promoting references to Java objects to global references when necessary.
The final technique we’ll discuss today is making an entire Swift library available to a Java application. It’s a very important use case because it allows us to implement crucial core business logic in Swift and use it in all our applications and services, regardless if they already adopted Swift or not yet.
We did mention earlier that interoperability needs to go both ways. It’s worth really internalizing that by making the Java-to-Swift direction a great experience, we enable more projects to use Swift and have a good time doing so, which is an important social aspect of introducing a new language to a codebase.
Now, if we were to approach exposing a whole Swift library using the previous techniques, we’d have to write lots of wrapper functions on the Java side. For a few functions, that’s fine, but if we’re talking about a whole library, it’s time to consider a different approach. Similar to what we’ve done in order to expose a Java library to Swift, now let’s make calling Swift as seamless and easy as possible for Java. We’ll do so by wrapping all types of our Swift library with Java classes and distributing it all together as a Java library. This time we won't be using JNI at all. Instead we’ll be using the new Foreign Function and Memory API that got stabilized in March last year and is now available since Java 22. This API offers improved control over native memory and how native calls are formed. In some situations it can be used as a JNI replacement. By making use of these new APIs we’re able to build a very deep integration between Java and Swift’s runtime and memory management. This would not have been possible otherwise. This results in improved safety and performance as we make native calls from Java. For this example, let’s use the Swift struct type that represents some business object I'd like to expose to Java. It is a value type and therefore does not have a stable object identity, which isn’t something Java objects can express. Because of that, we will have to be careful dealing with this object in Java. It also has a public property, an initializer, and some methods to operate on it. In order to expose this type to Java, we’ll be using the Swift-Java command line tool again. However, this time, we’ll use it in a slightly different mode. We’ll provide it a Swift input path and output directories for the generated Swift and Java sources. The tool will then take all sources from the input path and generate Java classes, which serve as accessors for the Swift types and functions. We will also generate some necessary Swift helper code.
Finally, everything, including the Swift code built as a dynamic library, will be compiled and packaged up into a Java library. The generated Java class will look something like this. It implements the Swift value interface because it was a Struct. The class contains a self-memory segment, which effectively is a pointer to the instance in the native memory.
It also represents all public initializers, properties, and functions using their equivalent Java signatures. Inside these, we have source-generated highly efficient code to make the native calls using the Foreign Function APIs.
Now inside our Java application, we can depend on the generate Java sources. In order to create and manage native Swift values, we’re going to need a SwiftArena, which takes care of memory allocation and lifetime of Swift objects. Once we have an arena prepared, we can just invoke the Swift values constructor, as if it were a normal Java class. Let’s take this opportunity to discuss a bit more how native and Java memory resources are being managed here.
First, a new Java wrapper object is allocated on the Java heap, which is managed by the JVM’s garbage collector. Its source-generated constructor then uses the passed in SwiftArena to allocate and initialize an instance of the Swift value type on the native heap. Normally value types like our SwiftyBusiness struct are allocated on the stack, but because we need a stable memory address, we allocate it on the heap instead. This allows us to safely point at this memory address from the Java wrapper object. Eventually, the Java wrapper is no longer used, and the Garbage Collector will decide to collect and destroy it. This will trigger a destroy of the native instance on the Swift side as well. So memory management wise, this is safe. However, unlike in Swift, relying on object finalization like this puts a large strain on the GC due to additional tracking it needs to perform to such objects. This also results in unpredictable timing of the native Swift value being de-initialized.
So while this is an easy pattern to get started, let me show you a better way to manage native memory.
Instead of using an Auto Arena, we can use a try-with-resources Java syntax in combination with a Confined Arena type. The object allocation will play out the same way here, however, the try-with-resources changes how objects are destroyed. Specifically, at the end of the triscope, the Arena will be closed. This triggers the destroy of the Java wrapper object, which in turn triggers the destroy of the Swift value on the native heap. This approach is much better. We don’t burden the GC with object finalization, which can be problematic when done in large numbers. And we also regained the property of well-defined and orderly object de-initialization, which many Swift programs rely on. So whenever possible, try to use Scoped Arenas, rather than relying on the GC for best application behavior and performance.
To recap what we achieved here; We were able to wrap a complete Swift library with just a single invocation of the Swift-Java command line tool. We’re able to build such Swift wrapped as Java libraries and even publish them, making them simple to consume in Java projects, further simplifying Swift adoption in your teams. And by using the modern Foreign Function and Memory APIs, we’re able to tightly control the object allocations and lifetimes, even of Swift value types. We covered a lot of different techniques to work with Swift and Java today, and you can use them independently or together, depending on the specific needs of your project.
Even though we’re just getting started and a lot remains to be polished and refined, SwiftJava already offers a great approach for interoperability between these languages. By using SwiftKit and JavaKit support libraries you can write safe and efficient code that uses one language from the other. And JavaKit macros as well as the Swift-Java command line tool automatically generate any boilerplate that would otherwise be difficult to maintain.
Finally I’d like to invite you to join us in the development of this project. It’s completely open source and available under the Swiftlang Github organization. There’s still lots of exciting challenges to solve and ideas to explore. If you’re not quite ready to contribute, but you’d like to learn more about Swift and Java or share your ideas and feedback, the best way is to join us on the Swift forums.
Thank you very much for joining me. And as for me, I think I’ll get myself a cup of coffee.
-
-
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 - Introduction & Agenda
Learn about an experimental Swift language library, called 'swift-java', that enables Swift to work seamlessly with Java, building on the existing interoperability features with Objective-C, C and C++. This interoperability allows you to incrementally introduce Swift into existing Java codebases, reuse libraries across languages, as well as integrate Swift libraries with Java projects. The interoperability provided by 'swift-java' supports both calling Java code from Swift and vice versa, and the team is working on tools and techniques to handle the differences between the Java and Swift runtimes, and provide better memory safety for Java code translated into Swift types.
- 2:41 - Runtime differences
Swift and Java share common features such as classes, inheritance, automatic memory management, generics, and error handling, though there are runtime differences. Despite these runtime differences, the similarities of both languages enable the expression of most APIs from one language in the other.
- 3:31 - Java Native methods
The Java Native Interface API (JNI) was introduced early in 1997 and enables Java code running inside a Java Virtual Machine (JVM) to interoperate with native code, such as Swift. This is often done to improve performance or utilize native libraries without Java equivalents. To use JNI, a 'native' function is defined in Java, and a corresponding C-header file is generated. This header file contains a C function declaration that must be implemented in the native language, like Swift. The process involves managing object lifetimes, matching method signatures, and dealing with verbose boilerplate code, which can be error-prone and time-consuming, leading to potential fatal crashes.
- 6:29 - SwiftJava
SwiftJava enhances the interoperability between Swift and Java languages. It provides a suite of tools, including Swift and Java libraries, a command-line tool, and build system integrations, to simplify and secure the interaction between the two languages. The 'swift-java' command-line tool automates the generation of bridging code, eliminating the need for manual C header interaction. This results in cleaner, more maintainable code with improved object lifetime management and type safety. You can now focus on business logic, leveraging the full power of both Swift and Java ecosystems, and avoiding the common pitfalls and errors associated with manual JNI implementation.
- 10:13 - Call Java from Swift
SwiftJava enables the integration of Java libraries into Swift projects. To import an entire Java library, such as Apache Commons CSV, prepare the following artifact coordinates: 'groupId', 'artifactId', and 'version'. SwiftJava then utilizes Gradle, an open source build system, to resolve Java dependencies. There are two methods for downloading and wrapping the Java dependencies: Using the SwiftJava build tool plugin, which requires disabling SwiftPM's security sandbox, or the 'swift-java' command-line tool's 'resolve' command, which performs the resolution outside of the build process. Once Java dependencies are resolved, you can import the Java library into Swift, start the JVM within the Swift process, and seamlessly use Java code and collections alongside Swift features, with JavaKit handling object lifetime management.
- 14:01 - Call Swift from Java
SwiftJava enables the integration of Swift libraries into Java projects. To achieve this, this WWDC25 session introduces a new approach that avoids the need for extensive wrapper functions on the Java side. Instead, the entire Swift library is wrapped with Java classes using the new Foreign Function and Memory API (FFI) introduced in Java 22. FFI provides improved control over native memory and enables a deep integration between the Java and Swift runtimes and memory management systems. By utilizing FFI, the process of calling Swift code from Java becomes more efficient and safe. The 'swift-java' command-line tool is employed to generate the necessary Java classes and Swift helper code. This tool automates boilerplate generation, making the process more straightforward. The generated Java classes serve as accessors for the Swift types and functions, effectively exposing the Swift library's functionality to Java. Proper memory management is crucial when working with native Swift objects in Java. The discussion highlights two approaches: using an 'AutoArena' which relies on the Java Garbage Collector (GC), and a preferred method using 'try-with-resources' and a 'ConfinedArena'. The latter approach ensures well-defined and orderly object de-initialization, avoiding performance issues and burdening the GC. This technique allows you to build and publish Swift-wrapped Java libraries, making them easily consumable in Java projects. This simplifies the adoption of Swift within teams that already have a strong Java presence, fostering a more flexible and efficient development environment. The ongoing development of SwiftJava aims to further polish and refine these techniques, providing a robust solution for interoperability between the two languages.
- 20:47 - Wrap up
The Swift programming language is open-source and hosted on GitHub, as well as libraries such as SwiftJava. The community is active on the Swift forums where you can learn, share ideas, and provide feedback.