Swiftpack.co - Package - SwiftJava/SwiftJava

SwiftJava - bridging Swift to a JavaVM

I know you've been thinking.. "What I really need is a way to bridge Swift to Java" but there are a number of use cases:

  1. Making Java technologies such as JDBC available to macOS applications.

  2. Giving Swift applications on Linux a portable user interface using Swing.

  3. Making business logic in written in Swift available to Android apps.

SwiftJava is a Swift code generator along with a small framework of supporting code written in the Xcode beta6 vintage of Swift 3.0. The starting point was Boris B├╝gling's Cross Platform Swift talk. The code generator takes a java class, interface or package and generates Swift classes that interface to corresponding Java methods using the Java Native Interface "JNI". These generated methods on the corresponding Swift class look something like this:

    /// public java.lang.String java.lang.Object.toString()

    private static var toString_MethodID_7: jmethodID?

    open func toString() -> String! {
        var __args = [jvalue]( repeating: jvalue(), count: 1 )
        let __return = JNIMethod.CallObjectMethod( object: javaObject, methodName: "toString", methodSig: "()Ljava/lang/String;", methodCache: &JavaObject.toString_MethodID_7, args: &__args, locals: nil )
        return JNIType.decode( type: String(), from: __return )
    }

On macOS, this has been used to generate frameworks bridging the java.lang, java.util, java.sql, java.awt and javax.swing packages along with the Apple specific additions in the com.apple package. This makes the Java apis available with auto-completion in the Xcode source editor. The final application can be a ".app" or a command line utility which should be portable to Linux using the Swift Package manager. For Android, the code generator can generate JNI code for a pair of interfaces from Java to Swift and from Swift to Java saving the developer the chore of a lot of error prone manual stubbing.

To use, clone this project using the following command:

    git clone https://github.com/SwiftJava/SwiftJava.git --recurse-submodules

This project contains the pre-generated java frameworks, an example macOS app using JDBC and a command line project with assorted AWT and Swing source. Development inside Xcode uses the legacy "JavaVM" framework which requires Apple's JVM downloadable from:

https://support.apple.com/kb/dl1572?locale=en_US

Development inside Xcode with the Oracle JVM is a little more complicated. It seems that JNI_CreateJavaVM generates a SIGSEGV internally as part of normal operation which is trapped using a signal handler so it can proceed on the command line. Unfortunately, this is caught by Xcode debugger lldb and it suspends and will not continue until you enter pr h -s false SIGSEGV into the debug console each time you run the program. The alternative is to not use the debugger at all in your scheme.

Perversely, with AWT and Swing on macOS the JVM needs to be created on the main thread while setup code needs to be off the main thread to leave it available for AWT's own runloop. Use the JNI.background and JNI.run methods to achieve this in a portable manner.

When using the Swift package manager to build code from the command line, install the latest Oracle JDK and locate the directory containing the file libjvm.so or .dylib in the jre. Use the examples. directory to build using the following commands:

    git clone https://github.com/SwiftJava/examples.git
    cd examples

    export JVM_LIBRARY_PATH=$JAVA_HOME/jre/lib/server # macOS
    export JVM_LIBRARY_PATH=$JAVA_HOME/jre/lib/amd64/server # Linux

    ulimit -n 10000 # increase file descriptors for link

    swift build -Xlinker -L$JVM_LIBRARY_PATH -Xlinker -rpath -Xlinker $JVM_LIBRARY_PATH -Xlinker -ljvm

The swing source in "examples/Sources" shows how to receive events and subclass a Java class to have certain methods such as java.awt.Canvas.paint() be implemented in Swift. More on this later.

Android Development

For Android, consult the modified versions of the swift-android-samples and the associated gradle build system plugin from the Android Toochain. This run on macOS or Ubuntu 16.04, and requires a Lollipop (api 21) or better device. More detaills and context are available in the Swift README for Android and this comprehensive tutorial but hopefully the scripts in the modified gradle plugin take most of the pain out of it.

Once you have a toolchain and run its setup.sh script you should be able to type the following commands:

    cd swift-android-samples/swifthello
    ./gradlew installDebug

For a new application, define two Java interfaces, one for messaging from Java to Swift with it's name ending in "Listener" and one for messaging back into Java from Swift. You then use ./genswift.sh from this project to generate the Swift binding code:

    ./genswift.sh your.package your.jar

This generates Swift classes and a third Java source src/org/swiftjava/your_package/YourAppProxy.java that also needs to be included in your project. Consult the script genhello.sh and project "swift-android-samples/swifthello" for details. The source "swift-android-samples/swifthello/src/main/swift/Sources/main.swift" shows how to set this up with a native method called from the main activity.

    import java_swift

    var responder: SwiftHelloResponderForward!

    class ListenerImpl: SwiftHelloListenerBase {

        override func processNumber( number: Double ) {
            responder.processedNumber( number+42.0 )
        }

        override func processText( text: String? ) {
            var out = String()
            for _ in 0..<100 {
                out += "Hello "+text!+"! "
            }
            responder.processedText( out )
        }

    }	

    @_silgen_name("Java_net_zhuoweizhang_swifthello_SwiftHello_bind")
    public func bind( __env: UnsafeMutablePointer<JNIEnv?>, __this: jobject?, __self: jobject? )-> jobject? {
        responder = SwiftHelloResponderForward( javaObject: __self )
        return ListenerImpl().javaObject
    }

Forward, Runnable, Listener, Adapter, subclass responsibility and Base classes.

For every Java interface the code generator generates a Swift Protocol along with a ProtocolForward class an instance of which conforms to the protocol and can be used to message Java instances conforming to the interface/protocol.

For the Runnable interface used in threading the converse needs to be possible where Java code can call through to Swift code. This is performed using a Java proxy class which has a pointer to the associated Swift object and a "native" implementation of the "run()" method that calls through to Swift. On the Swift side this is surfaced as the "RunnableBase" class which can be subclassed to provide a Swift implementation of the "run()" method callable from Java.

This approach is also taken for processing events and all interfaces with names ending in "Listener", "Manager" or "Handler" also have "Base" classes generated for subclassing along with Java Proxy classes. On macOS and Linux these classes are compiled into a jar file ~/swiftjava.jar using the genjar.sh script for this to work.

    class MyActionListener: ActionListenerBase {
        override func actionPerformed(e event: ActionEvent?) {
            System.exit(0);
        }
    }

    quitButton.addActionListener(MyActionListener());

Any interface/protocol from a Java interface can be added to a class to enable it to be passed to Java provide it's name ends in Listener. The implementation is a little complex but does not have an appreciable overhead. structs can also be made accesable to Java in this way but the object will no be live i.e. a coy of the struct will be taken.

Some event processing is also done by subclassing concrete classes that have names ending in "Adapter". Slightly modified Swift "Base" classes and Java Proxies are also generated for this. Other classes have methods that are intended to be responsibility of the subclasses such as java.awt.Canvas.paint(). A list of these methods needs to be maintained in the code generator unfortunately. If one of these methods encountered a Base class and Proxy is generated for the concrete class that can be subclassed.

As these "Base" subclasses can't close over variables in your program you may want to have an initialiser to capture these instead. There is a bit of a standard dance that needs to be performed. Instantiate the "Base" superclass and assign it's javaObject to your classes' javaObject. Due to the use of generics you'll also be prompted to provide a null implementation of the "required" initialiser.

class MyCanvas: CanvasBase {

    init(imageProducer:ImageProducer) {
        super.init(javaObject: nil)
        inherit(CanvasBase())
        image = createImage(imageProducer)
    }

    required init(javaObject: jobject?) {
        fatalError("init(javaObject:) has not been implemented")
    }

    ...

Consult the Swing examples code for further details.

MIT License

The MIT License (MIT) Copyright (c) 2016, John Holdsworth

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

This License does not apply to the code generated from the Apple distribution of the Java VM which are provided under the provisions of "Fair Use" and your use is ultimately subject to the original License Agreement.