Cangjie-Java Interoperability

Introduction

Note:

The Cangjie-Java interoperability feature is experimental and still under continuous improvement.

The Cangjie SDK supports interoperability between Cangjie and Java, enabling the development of business modules in Android applications using the Cangjie programming language. The overall calling process is as follows:

  • Scenario 1: Calling Cangjie from Java: Java code → (glue code) → Cangjie code
  • Scenario 2: Calling Java from Cangjie: Cangjie code → (glue code) → Java code

Key Concepts:

  1. Mirror Type: A representation of Java types using Cangjie syntax, allowing developers to call Java methods in a Cangjie-style manner.
  2. CFFI: C Foreign Function Interface, a low-level interoperability feature provided by high-level programming languages like Java/Objective-C/Cangjie for interacting with C.
  3. Glue Code: Intermediate code that bridges the differences between programming languages.
  4. Interop Code: Implementation code for Java calling Cangjie methods or vice versa.

Tools Involved:

  1. Cangjie SDK: A collection of developer tools for Cangjie.
  2. java-mirror-generator: A tool provided in the Cangjie SDK (file name: java-mirror-gen.jar) that automatically generates Mirror Types in Cangjie format based on Java .class files.
  3. cjc: Refers to the Cangjie compiler.
  4. Android Toolchain: The set of tools required for Android application development.

Interoperability between Cangjie and Java typically requires writing "glue code" using low-level interoperability features like CFFI in either Java or Cangjie. However, manually writing such glue code can be particularly tedious for developers.

The Cangjie compiler (cjc) included in the Cangjie SDK supports automatic generation of necessary glue code to reduce developer burden.

To generate glue code automatically, cjc requires symbol information about the Java types (classes and interfaces) involved in cross-language calls. This symbol information is contained in Mirror Types, which can be automatically generated using the java-mirror-generator (java-mirror-gen.jar) tool provided in the Cangjie SDK.

Taking Cangjie calling Java as an example, the overall development process is described as follows:

  1. Developer designs the interface, determining the function call flow and scope.

    Developer → Determine function call flow and scope

  2. Based on step 1, generate Mirror Types for the Java classes and interfaces to be called.

    .class → java-mirror-generator → Mirror Type.cj file

  3. Develop interop code, using the Mirror Types generated in step 2 to create Java objects and call Java methods.

    Interop Code.cj + Mirror Type.cj → Implement Cangjie calling Java

  4. Use cjc to compile the interop code and Mirror Type.cj files. cjc will generate:

    • Glue code.
    • Actual Java source code for interoperability.

    Mirror Type.cj + Interop Code.cj → cjc → Cangjie Code.so and Interop.java

  5. Add the files generated in step 4 to the Android project:

    • Interop.java: File generated by cjc.
    • Cangjie Code.so: File generated by cjc.
    • Runtime libraries included in the Cangjie SDK.

    Insert necessary calls in Java source code and rebuild the program.

    Android project + Interop.java + Cangjie Code.so → Android toolchain → .apk

Mirror Type Explanation:

Mirror Types contain declarations of classes and interfaces, providing symbol information about Java classes and interfaces to the Cangjie compiler cjc.

Taking the Java type com.example.a.A as an example:

// Java code
// src/java/com/example/a/A.java
package com.example.a;

public class A {
    private static int lastId = 0;
    private int id = lastId++;
    private String name;
    public A(String name) {
        this.name = name;
    }
    protected final int getId() {
        return id;
    }
    public String getName() {
        return name;
    }
}

The corresponding Mirror Type is as follows:

// Cangjie code
// src/cj/javaworld/src/A.cj
package javaworld

import java.lang.*

@JavaMirror["com.example.a.A"]
open class A {
    public init(arg0: ?JString) 
    protected func getId(): Int32
    public open func getName(): ?JString
}

As shown in the example, Mirror Types contain signatures of non-private members of the corresponding Java types, omitting the function bodies of methods/constructors.

Environment Setup

System Requirements:

  • Hardware/OS: Any system capable of running Android Studio.
  • Software: JDK 17, such as OpenJDK 17.

Setup Steps:

  1. Install the Cangjie SDK. Refer to the "Installing the Cangjie Toolchain" section for details.

  2. Install JDK 17.

    Note:

    This JDK is used as the execution environment for the Java Mirror generator tool java-mirror-gen.jar, not for building and running Android projects.

  3. Verify the location of java-mirror-gen.jar in the Cangjie SDK, e.g., /opt/cj-interop/java-mirror-gen.jar.

  4. Run the following command in the development environment console to verify successful installation:

    /path/to/jdk/17/bin/java -jar /path/to/java-mirror-gen.jar

    Example:

    /opt/openjdk-17.0.2/bin/java -jar /opt/cj-android-interop/java-mirror-gen.jar

    If the tool's usage instructions are printed, the installation is successful.

Version Compatibility:

  • The mirror generator runs on Java class files of version 61 or earlier, corresponding to Java 17, which is the version used in Android 14.
  • The special version of CJNative (Cangjie cross-compiler for Android) included in the toolkit corresponds to the mainstream version 0.60.4.

Usage Examples

Calling Cangjie from Java

Supported Parameter Types: Any Java type.

Supported Return Types: Any Java type or void.

Limitations:

  • Values of Java reference types cannot escape as global variables, static variables, or any data structures that persist between calls.
  • Java varargs methods and constructors are not supported.
  • Java generic types will be erased.
  • All involved Java reference types must be loaded by the same class loader.

Steps:

  1. Build your Android application as usual.

  2. Generate Mirror Types for Java types.

    Skip this step if only primitive types need to be passed/received. Primitive types refer to java.lang.Object, java.lang.String, or arrays of these types.

    Command:

    /path/to/jdk/17/bin/java \
        -Dpackage.mode -Dpackage.name=<package-name>  \
        -jar /path/to/toolkit/java-mirror-gen.jar \
        -cp <full-application-classpath> \
        -d <output-directory> \
        <names-of-mirrored-types>
    

    or

    /path/to/jdk/17/bin/java \
        -Dpackage.mode -Dpackage.name=<package-name> -Djar.mode[=true] \
        -jar /path/to/java-mirror-gen.jar \
        -cp <full-application-classpath> \
        -d <output-directory> \
        <jar-file>
    

    Where:

    • <package-name> is the package name for the generated Mirror Type.cj files.
    • <full-application-classpath> is the full dependency path of the Android application, including android.jar.
    • <output directory> is the directory where Mirror Type.cj files will be stored, e.g., src/cj.
    • <names-of-mirrored-types> are the fully qualified names of Java types for which Mirror Type.cj files will be generated. This takes effect when -Djar.mode is unset or -Djar.mode=false.
    • <jar-file> is the path to a single JAR file. This takes effect when -Djar.mode or -Djar.mode=true is set. In this mode, all .class files in <jar-file> and dependencies found in <full-application-classpath> will generate Mirror Type.cj files.

    Example where Java class Interop calls a Java function f() with parameters of type com.example.a.A, java.lang.String, and int, returning com.example.b.B, which calls a Cangjie-defined method:

    // Java code
    
    package cjworld;
    
    import com.example.a.A;
    import com.example.b.B;
    
    public class Interop {
        public static B f(A a, String s, int i) {
            /* Auto-generated glue code calls Cangjie method */
        }
    }
    

    Generate Mirror Types for the following Java types:

    /opt/jdk/17/bin/java -jar /opt/cj-android-interop/java-mirror-gen.jar \
        -cp /home/user/Android/Sdk/platforms/android-35/android.jar:App.jar \
        -d src/cj \
        com.example.a.A com.example.b.B
    

    This generates src/cj/com/example/a/A.cj (Mirror Type for com.example.a.A) and src/cj/com/example/a/B.cj (Mirror Type for com.example.a.B), along with Mirror Types for all dependency types (parameter/return value/parent type/non-private field types).

  3. Write the interop (Interop) class.

    The interop class refers to the Cangjie class called by Java. For example, InteropExample:

    1. Define an appropriate package and class name for the Cangjie class (the Java wrapper will have the same fully qualified name).
    2. Import java.lang.*.
    3. Import Mirror Types generated in step 2 (no need to import dependency types of Mirror Types).
    4. Add the @JavaImpl annotation to the class.
    5. Make the class inherit JObject or an Open Mirror Type.
    6. Use JObject, JString, and JArray<T> instead of java.lang.Object, java.lang.String, and Java array types. Other types use Java names.

    Java-to-Cangjie type mapping table (T' is a value type or Java Mirror Type listed in the table):

    Java Type (T) Cangjie Type (T')
    boolean Bool
    byte Int8
    short Int16
    char UInt16
    int Int32
    long Int64
    float Float32
    double Float64
    Object JObject or ?JObject
    String JString or ?JString
    class C C' or ?C'
    interface I I' or ?I'
    T[] JArray<T'> or ?JArray<T'>

    For Cangjie parameters, return values, Mirror Types, and local variables that may receive/hold Java null values, use Cangjie's ?<T'> (Option<T'>) type.

    Use Unit to represent the void return type of Java methods.

    Example:

    Continuing the example above, the InteropExample class is represented as:

    // Cangjie code
    package cjworld
    
    import java.lang.*
    import com.example.a.A // Mirror Type
    import com.example.b.B // Mirror Type
    
    @JavaImpl
    public class InteropExample <: JObject {
        public static func f(a: ?A, s: ?JString, i: Int32): ?B {
            /* Cangjie code */
        }
    }
    

    For types confirmed to be non-null, remove ?.

  4. Compile the interop (Interop) class.

    Compile the InteropExample class from step 3.

    Command:

    cjc --output-type=dylib \
        -p <source-directory> \
        -ljava.lang -linteroplib.interop \
        --output-javagen-dir=<java-output-directory>
    

    Where:

    • <source-directory> is the path to the directory containing interop class code (from step 3) and Mirror Type declarations (Cangjie files generated in step 2).

    • <java-output-directory> is the path to the directory where generated Java source files will be placed.

    The output includes the compiled interop class (.so file) and Java wrapper source code (.java file).

    Example:

    cjc --output-type=dylib \
        -p src/cj \
        -ljava.lang -linteroplib.interop \
        --output-javagen-dir=src/java
    

    The Cangjie compiler cjc generates two files: libcjworld.so and src/java/cjworld/InteropExample.java.

  5. Integrate into the Android application.

    Add the following files to your Android project:

    • Java source file InteropExample.java generated in step 4.
    • .so file libcjworld.so generated in step 4.
    • libc++_shared.so from the Android NDK.
    • All .so files in the $CANGJIE_HOME/runtime/lib/ directory and subdirectories.
    • $CANGJIE_HOME/lib/library-loader.jar.

    Then rebuild the Android project.

  6. Call Cangjie from Java.

    Add code to call Cangjie functions via the corresponding methods of the InteropExample class, then rebuild your Android project.

    Example:

    // Java code
    import com.example.a.A;
        ...
        B b = InteropExample.f(new A(), "Test", 0);
        ...
    

Calling Java from Cangjie

Supported Parameter/Return Types for Cangjie Calling Java:

Cangjie Type (T') Java Type (T) Remark
Bool boolean
Int8 byte
Int16 short
UInt16 char
Int32 int
Int64 long
Float32 float
Float64 double
JObject or ?JObject Object
JString or ?JString String
T' or ?T' T (*)
JArray<T'> or ?JArray<T'> T[] (**)

(*) T' must be a Mirror Type T of a Java type or an interop class whose Java interop class source code T is auto-generated by cjc.

(**) T' must be a Mirror Type, interop class, or one of the value types listed in the table (e.g., Int32).

Use Unit as the return type when calling Java methods that return void.

Limitations:

  • Java varargs methods and constructors are not supported.
  • All involved Java reference types must be loaded by the same class loader.

Steps:

  1. Build your Android application as usual.

  2. Generate Mirror Types.

    Skip this step if only primitive types need to be passed/received. Primitive types refer to java.lang.Object, java.lang.String, or arrays of these types.

    Command:

    /path/to/jdk/17/bin/java \
        -Dpackage.mode -Dpackage.name=<package-name> \
        -jar /path/to/toolkit/java-mirror-gen.jar \
        -cp <full-application-classpath> \
        -d <output-directory> \
        <names-of-mirrored-types>
    

    or

    /path/to/jdk/17/bin/java \
        -Dpackage.mode -Dpackage.name=<package-name> -Djar.mode[=true] \
        -jar /path/to/java-mirror-gen.jar \
        -cp <full-application-classpath> \
        -d <output-directory> \
        <jar-file>
    

    Where:

    • <full-application-classpath> is the full dependency path of the Android application, including android.jar.
    • <output directory> is the directory where Mirror Type.cj files will be stored, e.g., src/cj.
    • <names-of-mirrored-types> are the fully qualified names of Java types for which Mirror Type.cj files will be generated.
    • <package-name> is the package name for the generated Mirror Type.cj files.
    • <jar-file> is the path to a single JAR file. This takes effect when -Djar.mode or -Djar.mode=true is set. In this mode, all .class files in <jar-file> and dependencies found in <full-application-classpath> will generate Mirror Type.cj files.

    Example:

    Call a Java-implemented method com.example.c.C that takes two parameters of type com.example.a.A and int, returning String:

    // Java code
    package com.example.c;
    
    import com.example.a.A;
    
    public class C {
        public static String g(A a, int i) {
            /* Java code returning a string */
        }
    }
    

    Command:

    /opt/jdk/17/bin/java -jar /opt/cj-android-interop/java-mirror-gen.jar \
        -cp /home/user/Android/Sdk/platforms/android-35/android.jar:App.jar \
        -d src/cj \
        com.example.c.C
    

    This command will generate the Mirror Type for C src/cj/UNNAMED/src/com/example/c/C.cj, along with Mirror Types for all dependent types. Dependent types refer to parameter/return value/parent type/non-private field types.

    The generated src/cj/UNNAMED/src/com/example/c/C.cj is as follows:

    package com.example.c
    
    import java.lang.*
    import java.lang.JString
    
    public open class C {
        public init()
    
        public static func g(arg0: ?A, arg1: Int32): ?JString
    }
    
  3. Import Mirror Types and Invoke

    Import the Mirror Types generated in step 2; there is no need to import the dependent types of the Mirror Types.

    Call the functions of class C according to the Cangjie syntax.

    Example:

    // Cangjie code
    import javaworld.A
    import javaworld.C
        ...
        var maybe_s: ?JString = C.g(a, i)
        ...
    

    In the Usage Examples Calling Cangjie from Java section:

    // Cangjie code
    package cjworld
    
    import java.lang.*
    import javaworld.A
    import javaworld.B
    import javaworld.C
    
    @JavaImpl
    public class Interop <: JObject {
        public static func f(a: ?A, s: ?JString, i: Int32): ?B {
            let s1: JString = match (a) {
                case Some(aa) => C.g(aa, i) ?? JString("")
            }
            B(s1)
        }
    }
    
  4. Recompile the Cangjie Part

    Refer to step 4: Compile the Interop class.

  5. Update and Rebuild the Android Project

    Copy the .so and .java files generated in step 4 to the Android project and rebuild it.

Interop Library API Reference

The interop library includes java.lang.Object, java.lang.String, and java.lang.JArray.

java.lang.JObject

@JavaMirror["java.lang.Object"]
open class JObject {
    ...
    public func hashCode(): Int64
    @ForeignName["hashCode"]
    public open func hashCode32(): Int32
    ...
    public func toString(): String
    @ForeignName["toString"]
    public open func toJString(): JString
    ...
}
@ForeignName["hashCode"]
public open func hashCode32(): Int32
public func toString(): String
@ForeignName["toString"]
public open func toJString(): JString

The above public func hashCode(): Int64 corresponds to the hashCode method in Java.lang.Object, with a return type of Int64.

The above public open func hashCode32(): Int32 corresponds to the hashCode32 method in Java.lang.Object, with a return type of Int32.

The above public func toString(): String corresponds to the toString method in Java.lang.Object, with a return type of String.

java.lang.JString

@JavaMirror["java.lang.String"]
open class JString {
    ...
    public init(cjString: String)
    ...
}
public init(cjString: String)

The above public init(cjString: String) initializes a Java String (JString) object using a Cangjie String.

Note:

This constructor accepts a Cangjie-type parameter String. The cjc compiler has special handling for this function.

Methods inherited from JObject include: hashCode, hashCode32, toString, toJString.

java.lang.JArray

public init(length: Int32)
public prop length: Int32
public operator func [](index: Int32): T
public operator func [](index: Int32, value!: T): Unit

Features and Limitations

  1. Interop classes must be direct subclasses of Mirror Type. If the parent class of a Java class is java.lang.Object, the corresponding interop class must specify java.lang.JObject as its parent class.
  2. An interop class can implement one or more mirror interfaces but cannot implement non-mirror interfaces. Non-interop classes cannot implement or inherit mirror interfaces.
  3. Interop classes cannot be declared as open or abstract and cannot use extend.
  4. Interop classes can add fields of any Cangjie type and override member functions of their parent classes.
  5. Constructors of interop classes can call super().
  6. Instance member functions of interop classes can call instance member functions inherited from their mirror superclass and use super. to call overridden superclass member functions of the interop class.
  7. Mirror Type and interop class values: Cannot escape to Cangjie global variables, static variables, or variables referenced by global/static variables, otherwise it will cause undefined runtime behavior. The Cangjie compiler cjc currently does not perform compile-time checks for this restriction.

Android/JVM-specific limitations:

  1. Any code using Mirror Type or interop types must execute in a thread registered with the Java Virtual Machine. This can be a thread created in Java code or an O/S thread registered with the JVM using the Java Invocation API. The Cangjie compiler cjc currently does not perform compile-time checks for this restriction.
  2. All Java counterpart classes of Mirror Type and interop classes must be loaded by the same class loader.

JavaMirror Specification

JavaMirror provides a Cangjie syntax representation for Java types, automatically generated by tools, allowing developers to call Java methods in a Cangjie-style manner.

Note:

This feature is still experimental. Users should only use documented features. Using undocumented features may cause unknown errors such as compiler crashes.

Calling Constructors

Supports calling constructors of JavaMirror classes within JavaImpl classes.

@JavaMirror
public class Mirror {
    public init() 
    public init(other: Mirror) 
    public init(other: ?Mirror, deepCopy: Bool) 
}

// usage:
@JavaImpl
public class Main <: JObject {
    public init() {
        let mirror = Mirror()
        let other = Mirror(mirror)
    }
}

Inheritance

Java inheritance relationships can be mapped.

import java.lang.JObject

@JavaMirror
public open class Logger {
    public func log(msg: JString): Unit
}

@JavaMirror
public class EnhancedLogger <: Logger {
    public func verbose(msg: JString): Unit
}

Properties

JavaMirror classes support mutable properties, static properties, etc.

@JavaMirror
public class Mirror {
    public mut prop self: Mirror
    public mut prop selfOption: ?Mirror
    public static prop default: Mirror
    public static mut prop id: Int64
}

Corresponding Java code:

public class Mirror {
    public Mirror self;
    public Mirror selfOption;
    public static final Mirror default;
    public static long id;

    private Mirror() { /*...*/ }
}

Member Functions

JavaMirror member functions can have return values, parameters, and can be static methods.

@JavaMirror
class B {
    public func foo(): String
    public func modify(broken: Bool, upd: ?Mirror): Unit
    public func combine(other: Mirror): Mirror
    public static func getDefault(): Mirror
    public static func proxy(input: ?Mirror): Mirror
}

Note:

In the type mapping of return values, Java's String type can be implicitly converted to Cangjie's String type.

Interfaces

JavaMirror supports mapping Java interface types.

public interface I {
    public static long staticMethod() {
        return 1L;
    }
    public I foo();
    public long foo(I i);
}
@JavaMirror
public interface I {
    static func staticMethod(): Int64
    func foo(): I
    func foo(arg: I): Int64
}

Note:

Does not support default instance methods. Does not support handling fields in Java interfaces. Does not support handling properties in Mirror Types.

Abstract Classes

@JavaMirror supports mapping Java abstract classes, but currently cannot distinguish between abstract and non-abstract methods (future support planned).

Example:

package cj.com

@JavaMirror["java.com.AbstractMirror"]
abstract class AbstractMirror {
    init() 
    public static func staticFunc(): Unit
    protected open abstract func abstractFunc(): Unit
    public open func instanceFunc(): Unit
}

@JavaMirror["java.com.ImplAbstractMirror"]
open class ImplAbstractMirror <: AbstractMirror {
    init() 
    public func abstractFunc(): Unit
    public func id(x: AbstractMirror): AbstractMirror
    public func idImpl(x: ImplAbstractMirror): AbstractMirror
}

Corresponding Java code:

package java.com;

public abstract class AbstractMirror {
    public static void staticFunc() {
        System.out.println("Java: static func");
    }
    protected abstract void abstractFunc();
    public void instanceFunc() {
        System.out.println("Hello from instance func");
    }
}

public class ImplAbstractMirror extends AbstractMirror {
    public void abstractFunc() {
        System.out.println("abstractFunc()");
    }
    public AbstractMirror id(AbstractMirror x) {
        x.instanceFunc();
        return x;
    }

    public AbstractMirror idImpl(ImplAbstractMirror x) {
        x.instanceFunc();
        return x;
    }
}

Using the abstract class in Cangjie:

package cj.com

@JavaImpl["java.com.Impl"]
class Impl <: JObject {
    init() {
        let i = ImplAbstractMirror()
        let res = i.id(i)
        ImplAbstractMirror.staticFunc()

        match (res) {
            case AbstractMirror => println("id match: ok")
            case ImplAbstractMirror => println("id match: failed")
            case _ => println("id match: unexpected case")
        }

        match (res) {
            case ImplAbstractMirror => println("id match: ok")
            case AbstractMirror => println("id match: failed")
            case _ => println("id match: unexpected match")
        }

        res.abstractFunc()
        print("cast to ImplAbstractMirror: ", flush: true)
        (res as ImplAbstractMirror)?.abstractFunc()

        let res2 = i.idImpl(i)
    }
}
package java.com;

class Main {
    public static void main(String[] args) {
         var v = new Impl();
    }
}

Arrays

The JArray type supports array data mapping between Cangjie and Java, allowing conversion of arrays from Java to Cangjie or mapping Cangjie arrays to Java.

JArray provides the following capabilities:

  • Construct arrays of specific lengths.
  • Get array elements by index.
  • Set array elements by index.
  • Get array length.

Example:

// Main.java
package com.java.lib;

import cj.Impl;

public class Main {
    public static void main(String[] args) {
        JImpl impl = new Impl();
    }
}
// JImpl.Java
package com.java.lib;

public class JImpl {
    public JImpl() {
        System.out.println("java: JImpl()");
    }

    public void takeArr(long[] arr) {
        for (int i = 0; i < arr.length; i++) {
            System.out.println("java: " + i + "th of long[] - " + arr[i]);
        }
    };

    public long[] getArr() { long[] a = {6, 7, 13}; return a; };

    public void takeArr2(JImpl[] arr) {
        for (int i = 0; i < arr.length; i++) {
            System.out.println("java: " + i + "th of JImpl[] - " + arr[i].getInt());
        }
    };

    public JImpl[] getArr2() { JImpl[] a = {new JImpl(), new JImpl(), new JImpl()}; return a; };

    public long getInt() { return 55312; };
}
// Impl.cj

package cj

import interoplib.interop.*
import java.lang.JObject
import java.lang.JArray

@JavaMirror["com.java.lib.JImpl"]
open class JImpl <: JObject {
    public init() 
    public func takeArr(arr: JArray<Int64>): Unit
    public func getArr(): JArray<Int64>
    public func takeArr2(arr: JArray<JImpl>): Unit
    public func getArr2(): JArray<JImpl>
    public func getInt(): Int64
}

@JavaImpl
public class Impl <: JImpl {
    public func foo(): JArray<Int64> {
        JArray<Int64>(44)
    }

    public init() {
        println("cangjie: Impl()")

        let arr0 = JArray<Float64>(5)
        arr0[4] = 1.00033
        arr0[1] = -9.554
        println("cangjie: 5th of F64 array - " + arr0[4].toString())
        println("cangjie: 2nd of F64 array - " + arr0[1].toString())
        println("cangjie: length of F64 array - " + arr0
            .length
            .toString())

        let arr1 = JArray<JImpl>(9)
        arr1[1] = JImpl()
        println("cangjie: 2nd of JImpl array - " + arr1[1].getInt().toString())
        println("cangjie: length of JImpl array - " + arr1.length.toString())

        let arr2 = getArr()
        println("cangjie: 1st of I64 array - " + arr2[0].toString())
        println("cangjie: 2nd of I64 array - " + arr2[1].toString())
        println("cangjie: 3rd of I64 array - " + arr2[2].toString())
        arr2[2] = 73
        takeArr(arr2)

        let arr3 = getArr2()
        println("cangjie: 1st of JImpl array from Java - " + arr3[0].getInt().toString())
        println("cangjie: 2nd of JImpl array from Java - " + arr3[1].getInt().toString())
        println("cangjie: 3rd of JImpl array from Java - " + arr3[2].getInt().toString())
        arr3[2] = JImpl()
        takeArr2(arr3)

        let arr4 = this.foo()
        println("cangjie: length of I64 array - " + arr4
            .length
            .toString())
    }
}

Strings

The JString type supports string data mapping between Cangjie and Java, allowing conversion of strings from Java to Cangjie or mapping Cangjie strings to Java.

For example, the following example uses JString methods to map Cangjie strings to Java as function parameters:

@JavaMirror
class B {
    public func foo(s: JString): Unit
}

func useFoo() {
    let b = B()
    b.foo(JString("smth"))
}

In @JavaMirror classes, String can be used as a function return type, implicitly converting JString to Cangjie's String type.

// ...
class JObject {
    public func toString(): String

    // ...
}

Since all @JavaMirror/@JavaImpl types inherit from JObject, all types, including JString, have the toString method. This method supports explicit conversion of Java Strings to Cangjie Strings.

JavaImpl Specification

JavaImpl is a Cangjie annotation indicating that the methods and members of the class can be invoked by Java functions. During compilation, the compiler generates corresponding Java code for Cangjie classes annotated with @JavaImpl.

Note:

This feature is currently experimental. Users should only use the features documented herein. Using undocumented features may result in unknown errors such as compiler crashes.

Invoking JavaImpl Constructors

Constructors of JavaImpl classes can be invoked by other JavaImpl classes.

@JavaMirror
public class Handler {
    public prop isAlive: Bool
    public func enterWorkState(): Unit
}

@JavaImpl
public class Presenter <: JObject {
    public init(log: Bool) {
    }

    public func doLogics() {
        let handler = Handler()
        if (!handler.isAlive) {
            handler.enterWorkState()
        }
    }
}

@JavaImpl
public class Main <: JObject {
    public init() {
        // entry point
        let presenter = Presenter(true)
        presenter.doLogics()
    }
}

The constructor can also be invoked in Java:

Presenter p = Presenter(true);

Invoking JavaImpl Member Functions

Invoking JavaImpl member functions in Cangjie is the same as invoking regular Cangjie methods. To invoke them in Java:

@JavaMirror
public class Handler {
    public prop isAlive: Bool
    public func enterWorkState(): Unit
}

@JavaImpl
public class Presenter <: JObject {
    public init() {

    }

    public func doLogics(defaultHandler: ?Handler, log: Bool): Bool {
        let handler = defaultHandler ?? Handler()
        if (!handler.isAlive) {
            handler.enterWorkState()
            return true
        }
        return false
    }

    public static func isAlive(p: Presenter): Bool {
        ... // cangjie-side logics
    }
}
public class Handler {
    public boolean isAlive;
    ...
}

public class Main {
    public static void main(String[] args) {
        Presenter presenter = new Presenter();
        boolean result = p.doLogics(new Handler(), true);
        if (result) {
            System.out.println("Finished correctly");
        }

        System.out.println("is presenter alive: ");
        System.out.println(Presenter.isAlive(presenter));
    }
}

Defining Private Methods

For JavaImpl classes, pure Cangjie private methods (not callable by other types) can be added. These methods are not constrained to use only Java-compatible parameter and return types.

struct PureCangjieEntity {

}

@JavaImpl
public class Impl <: JObject {
    public Impl() {
        let entity = foo()
        ...
    }

    private func foo(): PureCangjieEntity {
        ... // do any logics
        return PureCangjieEntity()
    }
}

Type Matching and Type Casting

as, is, and type matching support @JavaMirror and @JavaImpl classes as type operands. In such cases, Java's instanceof function can be used to check the actual type. Additionally, subtype relationships in interfaces or inheritance chains are supported.

@JavaMirror
public class B {}

@JavaImpl
public class A {}

func foo(b: B) {
    println(b is A)
    println((b as A).isSome())

    match (b) {
        case b: A => println("A")
        case _ => ()
    }

    let c = Some(b)
    match (c) {
        case Some(b: A) => println("A")
        case _ => ()
    }
}

@ForeignName

The @ForeignName[".."] annotation can be applied to non-overridden methods in @JavaMirror and @JavaImpl classes.

  • When a method in a @JavaMirror class is annotated with @ForeignName["name"], the method and all its overrides will invoke the Java method name. Thus, name must match the method's signature in Java.
  • When a method in a @JavaImpl class is annotated with @ForeignName["name"], a method named name will be generated.
@JavaMirror
public class B {
    @ForeignName["bar"]
    public open func foo() // it's java's B.bar()
}

@JavaMirror
public class C <: B {
    public /* override */ func foo() // it's java's C.bar()
}

@JavaImpl
public class A <: C {
    // Java can call A.bar(), which is generated as bar()
    public /* override */ func foo() {
        // ...
    }
}

The corresponding Java code is:

class B {
    public void bar() {
        // ...
    }
}
class C extends B {
    @Override
    public void bar() { /* ... */ }
}

Java Usage of Cangjie Specification

New Experimental Compilation Option --experimental --enable-interop-cjmapping=<Target Languages>

Enables support for non-C language interoperation with Cangjie in the FE. Possible values for <Target Languages> are Java and ObjC.

Java Usage of Cangjie Structs

For interoperation between Cangjie and Java, support is needed for using Cangjie's struct data types in Java. As this feature is still under development, only the following scenarios are currently covered:

  1. Support for Java to call public instance methods and static methods of public Cangjie structs.
  2. Support for public Cangjie structs to be used as parameter types and return types in Java functions.

Example code:

// cangjie code

package cj

public struct Vector {
    private var x: Int32 = 0
    private var y: Int32 = 0

    public init(x: Int32, y: Int32) {
        this.x = x
        this.y = y
    }

    public func dot(v: Vector): Int64 {
        let res: Int64 = Int64(x * v.x + y * v.y)
        print("cj: Hello from dot (${x}, ${y}) . (${v.x}, ${v.y}) = ${res}\n", flush: true)
        return res;
    }

    public func add(v: Vector): Vector {
        let res = Vector(x + v.x, y + v.y)
        print("cj: Hello from add (${x}, ${y}) + (${v.x}, ${v.y}), new Vector = (${res.x}, ${res.y})\n", flush: true)
        return res
    }

    public static func hello(v: Vector): Unit {
        print("cj: Hello from static func in cj.Vector (${v.x}, ${v.y})\n", flush: true)
    }
}

Corresponding Java code:

package com.java.lib;

import cj.Vector;

public class Main {

    public static Vector getVector(int x, int y) {
        return new Vector(x, y);
    }

    public static void valueCheck(long value, long expectedValue) {
        if (value != expectedValue) {
            System.out.println("java: Test Failed, value: " + value + " != expected: " + expectedValue);
            System.exit(1); 
        }
    }

    public static void main(String[] args) {
        Vector.hello(getVector(0, 0));
        Vector u = new Vector(1, 2);
        Vector v = getVector(3, 4);
        long expectedValue = 1 * 3 + 2 * 4;
        valueCheck(u.dot(v), expectedValue);
        Vector w = u.add(v); // w = (4, 6)
        expectedValue = 4 * 1 + 6 * 2;
        valueCheck(w.dot(u), expectedValue);
        expectedValue = 3 * 4 + 4 * 6;
        valueCheck(v.dot(w), expectedValue);
        System.out.println("java: Correct result, dot value Test PASS.");
    }
}

Constraints

  1. Cangjie structs must not implement interfaces.
  2. Generic Cangjie structs are not yet supported.
  3. Access to struct member variables from Java is not yet supported.
  4. Mutating functions are not yet callable.
  5. Properties are not yet supported.

Java Usage of Cangjie Enums

Cangjie enum types need to be mapped to Java types to enable:

  1. Creating enum objects in Java by calling enum constructors.
  2. Passing enum objects across language boundaries.
  3. Invoking static or non-static methods defined in enums.
  4. Accessing enum properties.

Example code showing how Cangjie enums are mapped to Java classes:

// Cangjie
// =============================================
// Enum Definition: Basic Enum (TimeUnit)
// =============================================
public enum TimeUnit {
    | Year(Int64)
    | Month(Int64)
    | Year
    | Month

    // The public method to Calculate how many months.
    public func CalcMonth(): Unit {
        let s = match (this) {
            case Year(n) => "x has ${n * 12} months"
            case Year => "x has 12 months"
            case TimeUnit.Month(n) => "x has ${n} months"
            case Month => "x has 1 month"
        }
        println(s)
    }

    // The static method to return ten years.
    public static func TenYear(): TimeUnit {
        Year(10)
    }
}

Mapped Java code:

public class TimeUnit {
    static {
        loadLibrary("sampleEnum");
    }

    long self;

    private TimeUnit (long id) {
        self = id;
    }

    public static TimeUnit Year(long p1) {
        return new TimeUnit(YearinitCJObjectl(p1));
    }

    private static native long YearinitCJObjectl(long p1);

    public static TimeUnit Month(long p1) {
        return new TimeUnit(MonthinitCJObjectl(p1));
    }

    private static native long MonthinitCJObjectl(long p1);

    public static TimeUnit Year = new TimeUnit(YearinitCJObject());

    private static native long YearinitCJObject();

    public static TimeUnit Month = new TimeUnit(MonthinitCJObject());

    private static native long MonthinitCJObject();

    public void CalcMonth() {
        CalcMonth(this.self);
    }

    public native void CalcMonth(long self);

    public static native TimeUnit TenYear();

    public native void deleteCJObject(long self);

    @Override
    public void finalize() {
        deleteCJObject(this.self);
    }
}

Constraints

Support for combining enums with other language features is still under development. The following scenarios are currently unsupported:

  1. Cangjie enums must not implement interfaces.
  2. Cangjie enum member functions must not use generics.
  3. Cangjie enum member functions must not use lambdas.
  4. Cangjie enums must not contain operator overloading.
  5. Cangjie enums must only use basic data types.
  6. Cangjie enums must not be extended using extend.
  7. Option types are not supported.

Java Usage of Cangjie Extend

The non private members defined in the Cangjie extend syntax need to be mapped to Java to support users calling static or non static methods and properties defined in it on the Java side.

Example code showing how Cangjie extend are mapped to Java:

public class User {
    public let id: Int32
    public init(id: Int32) {
        this.id = id
    }
    public var a: Int32 = 0
    public static var b: Int32 = 0
}

extend User {
    public func getId(): Int32 {
        return id
    }
    public static func hello(): Unit {
    }
}

extend User {
    public mut prop pp: Int32 {
        get() {
            a
        }
        set(val) {
            a = val
        }
    }

    public static mut prop sp: Int32 {
        get() {
            b
        }
        set(val) {
            b = val
        }
    }
}

Mapped Java code:

public class User {
    public int getId() {
        return getId(this.self);
    }

    public native int getId(long self);

    public static native void hello();

    public int getPp() {
        return getPpImpl(this.self);
    }

    public native int getPpImpl(long self);

    public void setPp(int pp) {
        setPpImpl(this.self, pp);
    }

    public native void setPpImpl(long self, int pp);

    public static int getSp() {
        return getSpImpl();
    }

    public static native int getSpImpl();

    public static void setSp(int sp) {
        setSpImpl(sp);
    }

    public static native void setSpImpl(int sp);
}

Constraints

  • Support Direct Extensions and Interface Extensions
  • Extend defined members only support Cangjie basic data types
  • Interface Extensions not support @JavaMirror attribute related interfaces
  • Direct Extensions not support Operator overloading
  • Direct Extensions and Interface Extensions not support Generic