Cangjie-Python Interoperability

To be compatible with powerful computing and AI ecosystems, Cangjie supports interoperability with Python. The Python interoperability provides capabilities for users through the ffi.python library in the std module.

Currently, Python interoperability can be used only on the Linux platform and supports only the CJNative backend of the Cangjie compiler.

Global Resources and Usage of Python

Providing Built-in Function Classes and Global Resources

Code prototype:

public class PythonBuiltins {
    ...
}
public let Python = PythonBuiltins()

The interfaces provided by the Python library cannot ensure concurrency security. When Python is called asynchronously (the IDs of system threads are inconsistent), the PythonException exception is thrown.

During Python initialization, the GIL global interpreter lock is locked based on the current OS thread. If the Cangjie thread (including the main thread) where the executed code is located is locked, The Cangjie thread is scheduled on the OS thread (the ID of the OS thread changes). When Python attempts to check the GIL again, the thread status is verified and the GIL is found. The ID of the OS thread saved in the status is inconsistent with that of the current OS thread. As a result, an internal error is triggered and the program breaks down.

Python interoperability uses a large amount of native code of the Python library. Stack protection cannot be performed on the code on the Cangjie side. The default size of the Cangjie stack protection is 64 KB. When the Python C API is called, the native code may exceed the default stack size. As a result, overflow occurs and unexpected results are triggered. Before executing Python interoperability code, you are advised to set the default stack size of Cangjie to at least 1 MB: export cjStackSize=1MB.

Example:

import std.ffi.python.*

main(): Int64 {
    Python.load()
    Python.unload()
    return 0
}

Providing the Python Library Log Class PythonLogger

Code prototype:

public class PythonLogger <: Logger {
    mut prop level: LogLevel {...}
    public func setOutput(output: io.File): Unit {} // do nothing
    public func trace(msg: String): Unit {...}
    public func debug(msg: String): Unit {...}
    public func info(msg: String): Unit {...}
    public func warn(msg: String): Unit {...}
    public func error(msg: String): Unit {...}
    public func log(level: LogLevel, msg: String): Unit {...}
}
public let PYLOG = PythonLogger()

The Logger class is declared as follows:

  • The PythonLogger implements the Logger interface only for printing output and printing level control. Logs are not dumped to log files.
  • If setOutput is empty, log dump files are not supported.
  • The output of interfaces such as info/warn/error starts with the corresponding prefix.
  • The default print level of PythonLogger is LogLevel.WARN.
  • The PYLOG.error(msg) and log(LogLevel.ERROR, msg) interfaces throw the PythonException exception.

Example:

import std.ffi.python.*
import std.log.*

main(): Int64 {
    PYLOG.level = LogLevel.WARN // Only logs of the warn level and above are printed.
    PYLOG.info("log info")
    PYLOG.warn("log warn")
    try {
        PYLOG.error("log error")
    } catch(e: PythonException) {}

    PYLOG.log(LogLevel.INFO, "loglevel info")
    PYLOG.log(LogLevel.WARN, "loglevel warn")
    try {
        PYLOG.log(LogLevel.ERROR, "loglevel error")
    } catch(e: PythonException) {}
    return 0
}

Execution result:

WARN: log warn
ERROR: log error
WARN: loglevel warn
ERROR: loglevel error

Providing the Python Library Exception Class PythonException

Code prototype:

public class PythonException <: Exception {
    public init() {...}
    public init(message: String) {...}
}

The PythonException is described as follows:

  • The usage of PythonException is the same as that of the inherited Exception except the exception prefix.
  • When an internal exception occurs in Python, the external system can capture the exception through try-catch. If the exception is not captured, the exception stack is printed and the program exits. The return value is 1.

Example:

import std.ffi.python.*
import std.log.*

main(): Int64 {
    try {
        Python.load("/usr/lib/", loglevel: LogLevel.INFO)
    } catch(e: PythonException) {
        print("${e}") // PythonException: "/usr/lib/" does not exist or the file path is invalid.
    }
    return 0
}

Providing the Python Library Version Information Class Version

Code prototype:

public struct Version <: ToString {
    public init(major: Int64, minor: Int64, micro: Int64)
    public func getMajor(): Int64
    public func getMinor(): Int64
    public func getMicro(): Int64
    public func getVersion(): (Int64, Int64, Int64)
    public func toString(): String
}

The declaration of the Version class is as follows:

  • The Version version information consists of three parts: major version, minor version, and micro version.
  • The Version version is initialized only by using the constructor. Once defined, it cannot be modified.
  • The toString interface is provided for direct printing.
  • The getVersion interface is provided to obtain the tuple format of the version.

Example:

import std.ffi.python.*

main(): Int64 {
    Python.load()
    var version = Python.getVersion()
    print("${version}")
    var tuple_version = version.getVersion()
    Python.unload()
    return 0
}

PythonBuiltins Built-in Function Class

Importing and Loading the Python Library

Code prototype:

public class PythonBuiltins {
    public func load(loglevel!: LogLevel = LogLevel.WARN): Unit
    public func load(path: String, loglevel!: LogLevel = LogLevel.WARN): Unit
    public func isLoad(): Bool
    public func unload(): Unit
}
public let Python = PythonBuiltins()

The following statements about loading and unloading are as follows:

  • The load function is implemented in overloading mode. It can be loaded without participation or loaded from a specified dynamic library path. An optional parameter is provided to configure the PythonLogger print level. If the parameter is not configured, the PYLOG print level is reset to the warn print level.
  • The load() function is used for Python-related preparations and must be called before Python interoperability. For details about how to query the dynamic library, see the dynamic library loading policy.
  • For the load(path: String) function, you need to configure the dynamic library path path. path specifies the dynamic library file (for example, /usr/lib/libpython3.9.so) and cannot be set to a directory or a non-dynamic library file.
  • When the load function fails, the PythonException exception is thrown. If the program still needs to continue, pay attention to try-catch.
  • The unload function is called after Python interoperability is complete. Otherwise, related resources may be leaked.
  • The loading and unloading operations need to be called only once. If they are called for multiple times, only the first invoking takes effect.
  • The isload() function is used to determine whether the Python library is loaded.

Example:

load and unload:

import std.ffi.python.*

main(): Int64 {
    Python.load()
    Python.unload()
    Python.load("/usr/lib/libpython3.9.so")
    Python.unload()
    return 0
}

isLoad Function:

import std.ffi.python.*

main(): Int64 {
    print("${Python.isLoad()}\n")       // false
    Python.load()
    print("${Python.isLoad()}\n")       // true
    Python.unload()
    return 0
}

Loading Policy of Dynamic Library

The Python library depends on the official dynamic link library libpython3.x.so of Python. The recommended version is 3.9.2. Python3.0 and later versions are supported.

Obtaining the dynamic library from the Python source code:

# In the Python source code path:
./configure --enable-shared --with-system-ffi --prefix=/usr
make
make install

The dynamic library of Python is automatically searched as follows:

  1. Use the specified environment variable:
export PYTHON_DYNLIB=".../libpython3.9.so"
  1. If the environment variable is not specified, search for the dependency of the executable file.
  • Ensure that the executable file python3 can be executed properly (the path has been added to the PATH environment variable). Query the dynamic library dependency of the Python3 executable file.
  • Python executable files that are not dependent on dynamic libraries cannot be used. (Python executable files that are not compiled using --enable-shared during source code compilation do not depend on dynamic libraries.)
$ ldd $(which python3)
    ...
    libpython3.9d.so.1.0 => /usr/local/lib/libpython3.9d.so.1.0 (0x00007f499102f000)
    ...
  1. If the executable file dependency cannot be found, search for the dependency from the default dynamic library query path.
["/lib", "/usr/lib", "/usr/local/lib"]

The name of the dynamic library queried in the path must comply with the naming format of libpythonX.Y.so. X Y indicates the major version number and minor version number. The following suffixes are supported: d.so, m.so, dm.so, and .so. The supported version is later than Python 3.0 and earlier than or equal to Python 3.10. Example:

libpython3.9.so
libpython3.9d.so
libpython3.9m.so
libpython3.9dm.so

Example:

import std.ffi.python.*
import std.log.*

main(): Int64 {
    Python.load(loglevel: LogLevel.INFO)
    print("${Python.getVersion()}\n")
    Python.unload()
    return 0
}

You can enable the INFO level printing of Python and view the search process of the Python library path.

# Specifying .so by Using Environment Variables
$ export PYTHON_DYNLIB=/root/code/python_source_code/Python-3.9.2/libpython3.9d.so
$ cjc ./main.cj -o ./main && ./main
INFO: Try to get libpython path.
INFO: Found PYTHON_DYNLIB value: /root/code/python_source_code/Python-3.9.2/libpython3.9d.so
...

# Find dynamic libraries by executable file dependency.
INFO: Try to get libpython path.
INFO: Can't get path from environment PYTHON_DYNLIB, try to find it from executable file path.
INFO: Exec cmd: "ldd $(which python3)":
INFO:   ...
        libpython3.9d.so.1.0 => /usr/local/lib/libpython3.9d.so.1.0 (0x00007fbbb5014000)
        ...

INFO: Found lib: /usr/local/lib/libpython3.9d.so.1.0.
INFO: Found exec dependency: /usr/local/lib/libpython3.9d.so.1.0
...

# Search for the dynamic library in the system path.
$ unset PYTHON_DYNLIB
$ cjc ./main.cj -o ./main && ./main
INFO: Can't get path from environment PYTHON_DYNLIB, try to find it from executable file path.
INFO: Can't get path from executable file path, try to find it from system lib path.
INFO: Find in /lib.
INFO: Found lib: /lib/libpython3.9.so.
...

# Failed to find the dynamic library.
$ cjc ./main.cj -o ./main && ./main
INFO: Can't get path from environment PYTHON_DYNLIB, try to find it from executable file path.
INFO: Can't get path from executable file path, try to find it from system lib path.
INFO: Find in /lib.
INFO: Can't find lib in /lib.
INFO: Find in /usr/lib.
INFO: Can't find lib in /usr/lib.
INFO: Find in /usr/local/lib.
INFO: Can't find lib in /usr/local/lib.
An exception has occurred:
PythonException: Can't get path from system lib path, load exit.
         at std/ffi/python.std/ffi/python::(PythonException::)init(std/core::String)(stdlib/std/ffi/python/Python.cj:82)
         at std/ffi/python.std/ffi/python::(PythonBuiltins::)load(std/log::LogLevel)(stdlib/std/ffi/python/Python.cj:127)
         at default.default::main()(/root/code/debug/src/main.cj:5)

getVersion() Function

Function prototype:

public func getVersion(): Version

Interface description:

  • The getVersion() function is used to obtain the current Python version.

Return values of input parameters:

  • The getVersion() function has no parameter and returns a Version object.

Exceptions:

  • Ensure that the load function has been called. Otherwise, the returned version number is 0.0.0.

Example:

import std.ffi.python.*

main(): Int64 {
    Python.load()
    var version = Python.getVersion()
    print("${version}")
    var tuple_version = version.getVersion()
    Python.unload()
    return 0
}

Import() Function

Function prototype:

public func Import(module: String): PyModule

Return values of input parameters:

  • The Import function receives an input parameter of the String type, that is, a module name, and returns an object of the PyModule type.

Exceptions:

  • Ensure that the load function has been called for the Import function. Otherwise, the returned PyModule object is unavailable (isAvaliable() is false).
  • If the corresponding module cannot be found, an error is reported and the returned PyModule object is unavailable (isAvaliable() is false).

Example:

import std.ffi.python.*

main(): Int64 {
    Python.load()
    var sys = Python.Import("sys")
    if (sys.isAvailable()) {
        print("Import sys success\n")
    }
    // Import the test.py file in the current folder.
    var test = Python.Import("test")
    if (test.isAvailable()) {
        print("Import test success\n")
    }
    var xxxx = Python.Import("xxxx")
    if (!xxxx.isAvailable()) {
        print("Import test failed\n")
    }
    Python.unload()
    return 0
}

Execution result:

Import sys success
Import test success
Import test failed

Eval() Function

Function prototype:

public func Eval(cmd: String, module!: String = "__main__"): PyObj

Interface description:

  • The Eval() function is used to create a Python data type.

Return values of input parameters:

  • Eval() receives a cmd command of the String type and returns the PyObj format of the command result.
  • Eval() accepts a specified domain of the String type. The default domain is "__main__".

Exceptions:

  • Ensure that the load function has been called. Otherwise, the returned PyObj object is unavailable (isAvaliable() is false).
  • If the command received by Eval() fails to be executed, Python reports an error and the returned PyObj object is unavailable (isAvaliable() is false).

Example:

import std.ffi.python.*

main(): Int64 {
    Python.load()
    var a = Python.Eval("123")
    if (a.isAvailable()) {
        Python["print"]([a])
    }
    var b = Python.Eval("x = 123") // The expression in `Eval` needs have a return value.
    if (!b.isAvailable()) {
        print("b is unavailable.\n")
    }
    Python.unload()
    return 0
}

Execution result:

123
b is unavailable.

index [] Operator Overloading

Interface description:

  • The [] function provides the capability of calling other built-in functions of Python.

Return values of input parameters:

  • The input parameter of the [] function accepts the name of a built-in function of the String type, and the return type is PyObj.

Exception Handling:

  • Ensure that the load function has been called. Otherwise, the returned PyObj object is unavailable (isAvaliable() is false).
  • If the specified function name is not found, an error is reported and the returned PyObj object is unavailable (isAvaliable() is false).

Example:

import std.ffi.python.*

main(): Int64 {
    Python.load()
    if (Python["type"].isAvailable()) {
        print("find type\n")
    }
    if (!Python["type1"].isAvailable()) {
        print("cant find type1\n")
    }
    Python.unload()
    return 0
}

Execution result:

find type
WARN: Dict key "type1" not found!
cant find type1

Type Mapping

The interoperability between Python and Cangjie is developed based on C APIs. The data type mapping between Python and C is implemented through the PyObject structure pointer, and a series of interfaces for different data types are provided. Compared with the C language, Cangjie has the advantage of object-oriented programming. Therefore, the PyObject structure pointer is encapsulated as a parent class and inherited by different data types.

Type Mapping Table

Mapping from the Cangjie type to the Python type:

Cangjie TypePython Type
BoolPyBool
UInt8/Int8/Int16/UInt16/Int32/UInt32/Int64/UInt64PyLong
Float32/Float64PyFloat
Rune/StringPyString
Array< PyObj >PyTuple
ArrayPyList
HashMapPyDict
HashSetPySet

Mapping from the Python type to the Cangjie type:

Python TypeCangjie Type
PyBoolBool
PyLongInt64/UInt64
PyFloatFloat64
PyStringString
PyTuple-
PyListArray
PyDictHashMap
PySetHashSet

PyFFIType Interface of Python FFI Library Generic Constraint

public interface PyFFIType { }
  • Some classes introduce generics. To restrict the use of generics, the abstract interface PyFFIType is introduced.
  • This interface has no abstract member function and is implemented or inherited only by PyObj and CjObj. This interface cannot be implemented outside the package. If you customize a class and implement this interface, undefined behavior may occur.

PyObj Class

It corresponds to the PyObject structure in the Python library and provides common interfaces for subdivided data types, such as member variable access, function access, and character string conversion to Cangjie.

Class prototype:

public open class PyObj <: ToString & PyFFIType {
    public func isAvailable(): Bool { ... }
    public open operator func [](key: String): PyObj { ... }
    public open operator func [](key: String, value!: PyObj): Unit { ... }
    public operator func ()(): PyObj { ... }
    public operator func ()(kargs: HashMap<String, PyObj>): PyObj { ... }
    public operator func ()(args: Array<PyObj>): PyObj { ... }
    public operator func ()(args: Array<PyObj>, kargs: HashMap<String, PyObj>): PyObj { ... }
    public operator func ()(args: Array<CjObj>): PyObj { ... }
    public operator func ()(args: Array<CjObj>, kargs: HashMap<String, PyObj>): PyObj { ... }
    public operator func +(b: PyObj): PyObj { ... }
    public operator func -(b: PyObj): PyObj { ... }
    public operator func *(b: PyObj): PyObj { ... }
    public operator func /(b: PyObj): PyObj { ... }
    public operator func **(b: PyObj): PyObj { ... }
    public operator func %(b: PyObj): PyObj { ... }
    public open func toString(): String { ... }
    public func hashCode(): Int64 { ... }
    public operator func ==(right: PyObj): Bool { ... }
    public operator func !=(right: PyObj): Bool { ... }
}

Description of the PyObj Class

  • The PyObj class does not provide constructors for external systems. This class cannot be inherited outside the package. If a user customizes a class and implements the interface, undefined behavior may occur.

  • public func isAvailable(): Bool { ... } :

    • The isAvailable interface is used to determine whether the PyObj is available (that is, whether the encapsulated C pointer is NULL).
  • public open operator func [](key: String): PyObj { ... } :

    • [](key) is used to access members of a Python class or a module.
    • If PyObj is unavailable (isAvaliable() is false), an exception is thrown.
    • If key does not exist in PyObj, Python prints the corresponding error and returns an unavailable PyObj class object (isAvaliable() is false).
  • public open operator func [](key: String, value!: PyObj): Unit { ... } :

    • [](key, value): Set the member variables of the Python class and module to value.
    • If PyObj is unavailable (isAvaliable() is false), an exception is thrown.
    • If the PyObj does not contain the corresponding key, the Python side prints the corresponding error.
    • If the value of value is an unavailable object (isAvaliable() is false), the corresponding key is deleted from the module or class.
  • The () parenthesis operator is overloaded to call the function of the object.

    • If PyObj is unavailable (isAvaliable() is false), an exception is thrown.
    • If PyObj is an object that cannot be called, Python reports an error and returns an unavailable PyObj class object (isAvaliable() is false).
    • () accepts function calls without parameters.
    • The ([...]) parameter supports at least one parameter. The parameter type can be CjObj or PyObj. Note that CjObj and PyObj cannot be used together when multiple parameters are transferred.
    • If the parameter contains an unavailable object (isAvaliable() is false), an exception is thrown to prevent unpredictable program breakdown on the Python side.
    • The () operator supports kargs, which corresponds to the variable naming parameter design of Python. It is transferred through a HashMap. The key type String is configured as the variable name, and the value type PyObj is configured as the parameter value.
  • Binary operator overloading:

    • + (addition of two variables):

      • Basic data types: PyString and PyBool/PyLong/PyFloat cannot be added together. Other types can be added together.
      • Advanced data types: PyDict/PySet cannot be added to any type. PyTuple/PyList can only be added to itself.
    • - (subtraction of two variables):

      • Basic data types: PyString and PyBool/PyLong/PyFloat/PyString do not support subtraction. Other types support subtratcion.
      • Advanced data types: PyDict/PySet/PyTuple/PyList does not support subtraction from any type.
    • * (multiplication of two variables):

      • Basic data types: PyString and PyFloat/PyString do not support multiplication. Other types support multiplication.
      • Advanced data types: PyDict/PySet cannot be multiplied by any type. PyTuple/PyList can be multiplied only by PyLong/PyBool.
    • / (division of two variables):

      • Basic data types: PyString and PyBool/PyLong/PyFloat/PyString cannot be divided by each other. Other types can be divided by each other. If the divisor is 0 (False is interpreted as 0 on the Python side and cannot be used as a divisor), an error is printed on the Python side.
      • Advanced data types: PyDict/PySet/PyTuple/PyList cannot be divided by any type.
    • ** (exponent calculation):

      • Basic data types: PyString and PyBool/PyLong/PyFloat/PyString do not support exponential calculation. Other data types support exponential calculation.
      • Advanced data types: PyDict/PySet/PyTuple/PyList do not support exponentiation.
    • % (remainder operation):

      • Basic data types: PyString and PyBool/PyLong/PyFloat/PyString do not support the remainder operation. Other data types support the remainder operation. If the divisor is 0 (False is interpreted as 0 in Python and cannot be used as a divisor), an error message is displayed in Python.
      • Advanced data types: PyDict/PySet/PyTuple/PyList and all types do not support the modulo operation.
    • All the preceding errors are recorded at the warn level, and the returned PyObj is unavailable (isAvaliable() is false).

  • public open func toString(): String { ... } :

    • The toString function can return the Python data type as a string, and the basic data type is returned in Python style.
    • If PyObj is unavailable (isAvaliable() is false), an exception is thrown.
  • The hashCode function is the encapsulated Python hash algorithm and returns an Int64 hash value.

  • The == operator is used to determine whether two PyObj objects are the same. The != operator is used to determine whether two PyObj objects are the same. If the interface comparison fails, the == returns false and captures the Python error. If the two compared objects are unavailable, an exception is thrown.

Example:

test01.py file:

a = 10
def function():
    print("a is", a)
def function02(b, c = 1):
    print("function02 call.")
    print("b is", b)
    print("c is", c)

Cangjie file main.cj in the same directory:

import std.ffi.python.*
import std.collection.*

main(): Int64 {
    Python.load()

    // Create an unavailable value.
    var a = Python.Eval("a = 10")   // SyntaxError: invalid syntax
    print("${a.isAvailable()}\n")   // false

    // Uncallable value `b` be invoked
    var b = Python.Eval("10")
    b()                           // TypeError: 'int' object is not callable

    // Import .py file.
    var test = Python.Import("test01")

    // `get []` get value of `a`.
    var p_a = test["a"]
    print("${p_a}\n")               // 10

    // `set []` set the value of a to 20.
    test["a"] = Python.Eval("20")
    test["function"]()            // a is 20

    // Call function02 with a named argument.
    test["function02"]([1], HashMap<String, PyObj>([("c", 2.toPyObj())]))

    // Set `a` in test01 to an unavailable value, and `a` will be deleted.
    test["a"] = a
    test["function"]()            // NameError: name 'a' is not defined

    Python.unload()
    0
}

CjObj Interface

Interface prototype and type extension:

public interface CjObj <: PyFFIType {
    func toPyObj(): PyObj
}
extend Bool <: CjObj {
    public func toPyObj(): PyBool { ... }
}
extend Rune <: CjObj {
    public func toPyObj(): PyString { ... }
}
extend Int8 <: CjObj {
    public func toPyObj(): PyLong { ... }
}
extend UInt8 <: CjObj {
    public func toPyObj(): PyLong { ... }
}
extend Int16 <: CjObj {
    public func toPyObj(): PyLong { ... }
}
extend UInt16 <: CjObj {
    public func toPyObj(): PyLong { ... }
}
extend Int32 <: CjObj {
    public func toPyObj(): PyLong { ... }
}
extend UInt32 <: CjObj {
    public func toPyObj(): PyLong { ... }
}
extend Int64 <: CjObj {
    public func toPyObj(): PyLong { ... }
}
extend UInt64 <: CjObj  {
    public func toPyObj(): PyLong { ... }
}
extend Float32 <: CjObj  {
    public func toPyObj(): PyFloat { ... }
}
extend Float64 <: CjObj  {
    public func toPyObj(): PyFloat { ... }
}
extend String <: CjObj  {
    public func toPyObj(): PyString { ... }
}
extend<T> Array<T> <: CjObj where T <: PyFFIType {
    public func toPyObj(): PyList<T> { ... }
}
extend<K, V> HashMap<K, V> <: CjObj where K <: Hashable & Equatable<K> & PyFFIType {
    public func toPyObj(): PyDict<K, V> { ... }
}
extend<T> HashSet<T> <: CjObj where T <: Hashable, T <: Equatable<T> & PyFFIType {
    public func toPyObj(): PySet<T> { ... }
}

Description of the CjObj Class

The CjObj interface is implemented by all basic data types and toPyObj is extended. The CjObj interface can be converted to the corresponding Python data type.

Mapping between PyBool and Bool

Class prototype:

public class PyBool <: PyObj {
    public init(bool: Bool) { ... }
    public func toCjObj(): Bool { ... }
}

Description of the PyBool Class

  • The PyBool class is inherited from the PyObj class. The PyBool class has all the interfaces of the parent class.
  • PyBool can be constructed only by using the Bool type of Cangjie.
  • The toCjObj interface converts PyBool to the Cangjie data type Bool.

Example:

import std.ffi.python.*

main(): Int64 {
    Python.load()

    // Creation of `PyBool`.
    var a = PyBool(true)        // The type of `a` is `PyBool`.
    var b = Python.Eval("True") // The type of `b` is `PyObj` and needs to be matched to `PyBool`.
    var c = true.toPyObj()      // The type of `c` is `PyBool`, which is the same as `a`.

    print("${a}\n")
    if (a.toCjObj()) {
        print("success\n")
    }

    if (b is PyBool) {
        print("b is PyBool\n")
    }
    Python.unload()
    0
}

Execution result:

True
success
b is PyBool

Mapping between PyLong and Integers

Class prototype:

public class PyLong <: PyObj {
    public init(value: Int64) { ... }
    public init(value: UInt64) { ... }
    public init(value: Int32) { ... }
    public init(value: UInt32) { ... }
    public init(value: Int16) { ... }
    public init(value: UInt16) { ... }
    public init(value: Int8) { ... }
    public init(value: UInt8) { ... }
    public func toCjObj(): Int64 { ... }
    public func toInt64(): Int64 { ... }
    public func toUInt64(): UInt64 { ... }
}

Description of the PyLong Class

  • The PyLong class is inherited from the PyObj class. The PyLong class has all the interfaces of the parent class.

  • The PyLong supports the construction of input parameters of the integer type from all Cangjie.

  • The toCjObj and toInt64 interfaces convert PyLong to the Int64 type.

  • The toUInt64 interface converts PyLong to UInt64.

  • The PyLong type is converted to the Cangjie type to the 8-byte type. The PyLong type cannot be converted to the lower-byte type.

  • Overflow/Underflow problems:

    • If the original value of toInt64 (UInt64 is used to assign a value and no error is reported) exceeds the Int64 range, an overflow occurs.
    • If the original value of toUInt64 (Int64 is used to assign a value and no error is reported) exceeds the UInt64 range, an overflow occurs.
  • Currently, PyLong does not support large number processing.

Example:

import std.ffi.python.*

main(): Int64 {
    Python.load()

    // Creation of `PyLong`.
    var a = PyLong(10)          // The type of `a` is `PyLong`.
    var b = Python.Eval("10")   // The type of `b` is `PyObj` and needs to be matched to `PyLong`.
    var c = 10.toPyObj()        // The type of `c` is `PyLong`, which is the same as `a`.

    print("${a}\n")
    if (a.toCjObj() == 10 && a.toUInt64() == 10) {
        print("success\n")
    }

    if (b is PyLong) {
        print("b is PyLong\n")
    }
    Python.unload()
    0
}

Execution result:

10
success
b is PyLong

Mapping between PyFloat and Floating Points

Class prototype:

public class PyFloat <: PyObj {
    public init(value: Float32) { ... }
    public init(value: Float64) { ... }
    public func toCjObj(): Float64 { ... }
}

Description of the PyFloat Class

  • The PyFloat class is inherited from the PyObj class. The PyFloat class has all the interfaces of the parent class.
  • PyBool can be constructed using data of the Cangjie Float32/Float64 type.
  • To ensure precision, the toCjObj interface converts PyFloat to the Cangjie data type Float64.

Example:

import std.ffi.python.*

main(): Int64 {
    Python.load()

    // Creation of `PyLong`.
    var a = PyFloat(3.14)       // The type of `a` is `PyFloat`.
    var b = Python.Eval("3.14") // The type of `b` is `PyObj` and needs to be matched to `PyFloat`.
    var c = 3.14.toPyObj()      // The type of `c` is `PyFloat`, which is the same as `a`.

    print("${a}\n")
    if (a.toCjObj() == 3.14) {
        print("success\n")
    }

    if (b is PyFloat) {
        print("b is PyFloat\n")
    }
    Python.unload()
    0
}

Execution result:

3.14
success
b is PyFloat

Mapping between PyString and Characters and Character Strings

Class prototype:

public class PyString <: PyObj {
    public init(value: String) { ... }
    public init(value: Rune) { ... }
    public func toCjObj(): String { ... }
    public override func toString(): String { ... }
}

Description of the PyString Class

  • The PyString class is inherited from the PyObj class. The PyString class has all the interfaces of the parent class.
  • PyString can be constructed using data of the Cangjie Rune/String type.
  • The toCjObj/toString interface is used to convert PyString to the String data type.

Example

import std.ffi.python.*

main(): Int64 {
    Python.load()

    // Creation of `PyString`.
    var a = PyString("hello python")        // The type of `a` is `PyString`.
    var b = Python.Eval("\"hello python\"") // The type of `b` is `PyObj` and needs to be matched to `PyString`.
    var c = "hello python".toPyObj()        // The type of `c` is `PyString`, which is the same as `a`.

    print("${a}\n")
    if (a.toCjObj() == "hello python") {
        print("success\n")
    }

    if (b is PyString) {
        print("b is PyString\n")
    }
    Python.unload()
    0
}

Execution result:

hello python
success
b is PyString

PyTuple Type

Class prototype:

public class PyTuple <: PyObj {
    public init(args: Array<PyObj>) { ... }
    public operator func [](key: Int64): PyObj { ... }
    public func size(): Int64 { ... }
    public func slice(begin: Int64, end: Int64): PyTuple { ... }
}

Description of the PyTuple Class

  • The PyTuple type is the same as the tuple type in Python, that is, the (...) variable is used in Python code.
  • The PyTuple class is inherited from the PyObj class. The PyTuple class has all the interfaces of the parent class.
  • PyTuple can be constructed using Cangjie Array. The element type of Array must be PyObj (different data types of Python can be transferred using PyObj, that is, different data types of different elements in Tuple are compatible). If a member contains an unavailable object, an exception is thrown.
  • Overloading of the [] operator:
    • The input parameter type of [] in the parent class PyObj is String. When this class is called, the internal member variables or functions of the Python tuple type can be accessed or set.
    • The subclass PyTuple can use [] to access elements. If the badge key exceeds the range of [0, size ()), an error is reported and an unavailable PyObj object is returned.
    • The set [] operator is not reloaded because the Python tuple is an immutable object.
  • The size function is used to obtain the length of PyTuple.
  • The slice function is used to tailor the source PyTuple and return a new PyTuple. If the input parameters begin and end of slice are not in the range of [0, size ()), the source PyTuple is still tailored.

Example:

import std.ffi.python.*

main(): Int64 {
    Python.load()

    // Creation of `PyTuple`.
    var a = PyTuple(["Array".toPyObj(), 'a'.toPyObj(), 1.toPyObj(), 1.1.toPyObj()])
    var b = match (Python.Eval("('Array', 'a', 1, 1.1)")) {
        case val: PyTuple => val
        case _ => throw PythonException()
    }

    // Usage of size
    println(a.size())           // 4

    // Usage of slice
    println(a.slice(1, 2))      // ('a',). This print is same as Python code `a[1: 2]`.
    println(a.slice(-1, 20))    // ('Array', 'a', 'set index 3 to String', 1.1)

    Python.unload()
    return 0
}

Execution result:

4
('a',)
('Array', 'a', 1, 1.1)

Mapping between PyList and Array

Class prototype:

public class PyList<T> <: PyObj where T <: PyFFIType {
    public init(args: Array<T>) { ... }
    public operator func [](key: Int64): PyObj { ... }
    public operator func [](key: Int64, value!: T): Unit { ... }
    public func toCjObj(): Array<PyObj> { ... }
    public func size(): Int64 { ... }
    public func insert(index: Int64, value: T): Unit { ... }
    public func append(item: T): Unit { ... }
    public func slice(begin: Int64, end: Int64): PyList<T> { ... }
}

Description of the PyList Class

  • The PyList class is the same as the list type in Python. That is, the Python code uses the [...] variable.

  • The PyList class is inherited from the PyObj class. The PyList class has all interfaces of the parent class. This class maps the array of the Cangjie class. Therefore, the generic T class is introduced. The T type is restricted to the subclass of the PyFFIType interface.

  • The PyList class can be constructed based on the Array type of Cangjie. The member type of Array is also restricted to the subclass of the PyFFIType interface.

  • Overloading of the [] operator:

    • In the parent class PyObj, the input parameter type of [] is String. When this class object is called, only the internal member variables or functions of Python can be accessed or set.
    • The input parameter type of [] in this class is Int64, that is, the corner mark value of Array. The value range is [0, size ()). If the input parameter is not within the range, an error is reported and the returned object is unavailable.
    • [] also supports get and set. When set is used, the value type is T. If the value contains an unavailable Python object, an exception is thrown.
  • The toCjObj function can convert PyList to Array<PyObj> of Cangjie. Note that PyList is not converted to Array<T>.

  • The size function returns the length of PyList.

  • The insert function inserts value at the position of index and moves the elements following value backward. If the index is not in the range of [0, size ()), value can be inserted normally. If value is an unavailable object, an exception is thrown.

  • The append function appends item to PyList. If value is an unavailable object, an exception is thrown.

  • The slice function is used to truncate data in the range of [begin, end) and return a new PyList, begin and end that are not in the range of [0, size ()).

Example:

import std.ffi.python.*

main(): Int64 {
    Python.load()

    // Creation of `PyList`.
    var a = PyList<Int64>([1, 2, 3])
    var b = match (Python.Eval("[1, 2, 3]")) {
        case val: PyList<PyObj> => val
        case _ => throw PythonException()
    }
    var c = [1, 2, 3].toPyObj()

    // Usage of `[]`
    println(a["__add__"]([b]))   // [1, 2, 3, 1, 2, 3]
    a[1]
    b[1]
    a[2] = 13
    b[2] = 15.toPyObj()

    // Usage of `toCjObj`
    var cjArr = a.toCjObj()
    for (v in cjArr) {
        print("${v} ")          // 1 2 13
    }
    print("\n")

    // Usage of `size`
    println(a.size())           // 3

    // Usage of `insert`
    a.insert(1, 4)              // [1, 4, 2, 13]
    a.insert(-100, 5)           // [5, 1, 4, 2, 13]
    a.insert(100, 6)            // [5, 1, 4, 2, 13, 6]
    b.insert(1, 4.toPyObj())    // [1, 4, 2, 15]

    // Usage of `append`
    a.append(7)                 // [5, 1, 4, 2, 13, 6, 7]
    b.append(5.toPyObj())       // [1, 4, 2, 15, 5]

    // Usage of `slice`
    a.slice(1, 2)               // [1]
    a.slice(-100, 100)          // [5, 1, 4, 2, 13, 6, 7]
    b.slice(-100, 100)          // [1, 4, 2, 15, 5]

    return 0
}

Execution result:

[1, 2, 3, 1, 2, 3]
1 2 13
3

Mapping between PyDict and HashMap

Class prototype:

public class PyDict<K, V> <: PyObj where K <: Hashable & Equatable<K> & PyFFIType {
    public init(args: HashMap<K, V>) { ... }
    public func getItem(key: K): PyObj { ... }
    public func setItem(key: K, value: V): Unit { ... }
    public func toCjObj(): HashMap<PyObj, PyObj> { ... }
    public func contains(key: K): Bool { ... }
    public func copy(): PyDict<K, V> { ... }
    public func del(key: K): Unit { ... }
    public func size(): Int64 { ... }
    public func empty(): Unit { ... }
    public func items(): PyList<PyObj> { ... }
    public func values(): PyList<PyObj> { ... }
    public func keys(): PyList<PyObj> { ... }
}

Description of the PyDict Class

  • The PyDict is the same as the Python dictionary type, that is, the { a: b} variable is used in the Python code.

  • The PyDict class is inherited from the PyObj class. The PyDict class has all interfaces of the parent class. This class maps the HashMap of the Cangjie class. Therefore, the generic <K, V> class is introduced. The K type is a subclass of the PyFFIType interface, can be calculated by the Hash class, and the == and != operators are reloaded.

  • The PyDict receives data from the HashMap of the Cangjie type for construction.

    • K can only be of the CjObj or PyObj type or its subclasses.
    • The values of the same Python data are the same. For example, Python.Eval("1") and 1.toPyObj() are in the == relationship.
  • The getItem function is used to obtain the value of the key value corresponding to PyDict. If the key value cannot be found, an error is reported and an unavailable PyObj is returned. If the configured value key or value is of the PyObj type and is unavailable, an exception is thrown.

  • The setItem function is used to configure value of the key value corresponding to PyDict. If the key value cannot be found, it is inserted. If the configured value key or value is of the PyObj type and is unavailable, an exception is thrown.

  • The toCjObj function is used to convert PyDict to HashMap<PyObj, PyObj>.

  • The contains function is used to check whether the value of key is contained in the current dictionary. The return value is of the Boolean type. If the interface fails to be invoked, an error is reported and false is returned.

  • The copy function is used to copy the current dictionary and return a new PyDict<T> type. If the copy fails, the returned PyDict is unavailable.

  • The del function is used to delete the value of the corresponding key. If the key value is of the PyObj type and is unavailable, an exception is thrown.

  • The size function is used to return the length of the current dictionary.

  • The empty function is used to clear the current dictionary content.

  • The items function is used to obtain a list of key-value pairs of the Python list type, which can be iteratively accessed.

  • The values function is used to obtain a list of values of the Python list type, which can be iteratively accessed.

  • The keys function is used to obtain a list of Python keys of the list type, which can be iteratively accessed.

Example:

import std.ffi.python.*
import std.collection.*

main() {
    Python.load()

    // Creation of `PyDict`
    var a = PyDict(HashMap<Int64, Int64>([(1, 1), (2, 2)]))             // The key type is `CjObj`.
    var b = PyDict(HashMap<PyObj, Int64>([(Python.Eval("1"), 1), (Python.Eval("2"), 2)]))   // The key type is `PyObj`.
    var c = match (Python.Eval("{'pydict': 1, 'hashmap': 2, 3: 3, 3.1: 4}")) {
        case val: PyDict<PyObj, PyObj> => val       // Python side return `PyDict<PyObj, PyObj>`
        case _ => throw PythonException()
    }
    var d = HashMap<Int64, Int64>([(1, 1), (2, 2)]).toPyObj()

    // Usage of `getItem`
    println(a.getItem(1))               // 1
    println(b.getItem(1.toPyObj()))     // 1

    // Usage of `setItem`
    a.setItem(1, 10)
    b.setItem(1.toPyObj(), 10)
    println(a.getItem(1))               // 10
    println(b.getItem(1.toPyObj()))     // 10

    // Usage of `toCjObj`
    var hashA = a.toCjObj()
    for ((k, v) in hashA) {
        print("${k}: ${v}, ")           // 1: 10, 2: 2,
    }
    print("\n")
    var hashB = b.toCjObj()
    for ((k, v) in hashB) {
        print("${k}: ${v}, ")           // 1: 10, 2: 2,
    }
    print("\n")

    // Usage of `contains`
    println(a.contains(1))              // true
    println(a.contains(3))              // false
    println(b.contains(1.toPyObj()))    // true

    // Usage of `copy`
    println(a.copy())                   // {1: 10, 2: 2}

    // Usage of `del`
    a.del(1)                            // Delete the key-value pair (1: 1).

    // Usage of `size`
    println(a.size())                   // 1

    // Usage of `empty`
    a.empty()                           // Clear all elements in dict.

    // Usage of `items`
    for (i in b.items()) {
        print("${i} ")                  // (1, 10) (2, 2)
    }
    print("\n")

    // Usage of `values`
    for (i in b.values()) {
        print("${i} ")                  // 10 2
    }
    print("\n")

    // Usage of `keys`
    for (i in b.keys()) {
        print("${i} ")                  // 1, 2
    }
    print("\n")

    Python.unload()
}

Mapping between PySet and HashSet

Class prototype:

public class PySet<T> <: PyObj where T <: Hashable, T <: Equatable<T> & PyFFIType {
    public init(args: HashSet<T>) { ... }
    public func toCjObj(): HashSet<PyObj> { ... }
    public func contains(key: T): Bool { ... }
    public func add(key: T): Unit { ... }
    public func pop(): PyObj { ... }
    public func del(key: T): Unit { ... }
    public func size(): Int64 { ... }
    public func empty(): Unit { ... }
}

Description of the PySet Class

  • PySet corresponds to the data type of the collection in Python. When an element is inserted, the hash algorithm in Python is used to sort the elements in the collection. (The elements may not be sorted in strict ascending order. Some methods may cause different running results.)

  • The PySet class is inherited from the PyObj class. The PySet class has all interfaces of the parent class. This class maps the HashSet of the Cangjie class. Therefore, the generic T is introduced. The T type is a subclass of the PyFFIType interface, can be calculated by the Hash class, and the == and != operators are reloaded.

  • The PySet receives data from the HashMap of the Cangjie type for construction.

    • K can only be of the CjObj or PyObj type or its subclasses.
    • The values of the same Python data are the same. For example, Python.Eval("1") and 1.toPyObj() are in the == relationship.
  • The toCjObj function is used to convert PySet<T> to HashSet<PyObj>. Note that only the element type PyObj can be converted.

  • The contains function is used to determine whether key exists in the current dictionary. The key type is T.

  • The add function can be used to insert a value. If a key value already exists in PySet, the insertion does not take effect. If key is PyObj and is unavailable, an exception is thrown.

  • The pop function is used to obtain the first element from PySet.

  • Delete the corresponding key value from del. If key is not in PySet, an error is reported and the system exits normally. If key is PyObj and is unavailable, an exception is thrown.

  • size is used to return the length of PySet.

  • The empty command is used to clear the current PySet.

Note:

After toCjObj is called, all elements are displayed by pop. In this case, the original PySet is empty (size is 0, and the original PySet is still available).

Example:

import std.ffi.python.*
import std.collection.*

main() {
    Python.load()

    // Creation of `PySet`
    var a = PySet<Int64>(HashSet<Int64>([1, 2, 3]))
    var b = match (Python.Eval("{'PySet', 'HashSet', 1, 1.1, True}")) {
        case val: PySet<PyObj> => val
        case _ => throw PythonException()
    }
    var c = HashSet<Int64>([1, 2, 3]).toPyObj()

    // Usage of `toCjObj`
    var cja = a.toCjObj()
    println(a.size())                           // 0

    // Usage of `contains`
    println(b.contains("PySet".toPyObj()))      // true

    // Usage of `add`
    a.add(2)
    println(a.size())   // 1
    a.add(2)            // Insert same value, do nothing.
    println(a.size())   // 1
    a.add(1)            // Insert `1`.

    // Usage of `pop`
    println(a.pop())    // 1. Pop the first element.
    println(a.size())   // 1

    // Usage of `del`
    c.del(2)
    println(c.contains(2))  // false

    // Usage of `empty`
    println(c.size())   // 2
    c.empty()
    println(c.size())   // 0

    Python.unload()
}

PySlice Type

The usage of the PySlice type is the same as that of the return value of the Python built-in function slice(). The PySlice type can be used to identify a range and step, and can be used as the index value of the type that can be sliced to tailor and obtain the substring. To facilitate construction on the Cangjie side, the PySlice class and the Range range type of Cangjie can be converted to each other. For details, see the following description.

Class prototype:

public class PySlice<T> <: PyObj where T <: Countable<T> & Comparable<T> & Equatable<T> & CjObj {
    public init(args: Range<T>) { ... }
    public func toCjObj(): Range<Int64> { ... }
}

Description of PySlice

  • The PySlice can be constructed using the Range type of Cangjie and supports the syntax sugar of the Range type. The generic T has the original Range constraint and the constraint is added to the implementation of the CjObj type. The PyObj type is not supported.
  • The toCjObj function can be used to convert PySlice to Cangjie Range. Note that the generic type of Range is an integer of the Int64 type.
  • If you want to transfer the PySlice type to PyString/PyList/PyTuple or other PyObj types that can be transferred by slice, you can use the member function __getitem__ to transfer the PySlice type. For details, see the example.

Example:

import std.ffi.python.*

main() {
    Python.load()
    var range = 1..6:2

    // Create a PySlice.
    var slice1 = PySlice(range)
    var slice2 = match (Python["slice"]([0, 6, 2])) {
        case val: PySlice<Int64> => val
        case _ => throw PythonException()
    }
    var slice3 = range.toPyObj()

    // Use PySlice in PyString.
    var str = PyString("1234567")
    println(str["__getitem__"]([range]))    // 246
    println(str["__getitem__"]([slice1]))   // 246

    // Use PySlice in PyList.
    var list = PyList(["a", "b", "c", "d", "e", "f", "g", "h"])
    println(list["__getitem__"]([range]))   // ['b', 'd', 'f']
    println(list["__getitem__"]([slice1]))  // ['b', 'd', 'f']

    // Use PySlice in PyTuple.
    var tup = PyTuple(list.toCjObj())
    println(tup["__getitem__"]([range]))    // ('b', 'd', 'f')
    println(tup["__getitem__"]([slice1]))   // ('b', 'd', 'f')

    Python.unload()
    0
}

Execution result:

246
246
['b', 'd', 'f']
['b', 'd', 'f']
('b', 'd', 'f')
('b', 'd', 'f')

PyObjIterator of PyObj

Code prototype:

Extension of PyObj:

extend PyObj <: Iterable<PyObj> {
    public func iterator(): Iterator<PyObj> { ... }
}

PyObjIterator type:

public class PyObjIterator <: Iterator<PyObj> {
    public init(obj: PyObj) { ... }
    public func next(): Option<PyObj> { ... }
    public func iterator(): Iterator<PyObj> { ... }
}

Description of PyObjIterator

  • You can obtain PyObjIterator by using the iterator method of PyObj.

  • The PyObjIterator can be constructed externally. If the provided PyObj cannot be iterated or is unavailable, an exception is thrown.

    • The PyString/PyTuple/PyList/PySet/PyDict object can be iterated.
    • When PyDict is iterated directly, the value of key is iterated.
  • The next function is used to iterate the iterator.

  • The iterator method is used to return itself.

Example

import std.ffi.python.*
import std.collection.*

main() {
    Python.load()

    // iter of PyString
    var S = PyString("Str")
    for (s in S) {
        print("${s} ")      // S t r
    }
    print("\n")

    // iter of PyTuple
    var T = PyTuple(["T".toPyObj(), "u".toPyObj(), "p".toPyObj()])
    for (t in T) {
        print("${t} ")      // T u p
    }
    print("\n")

    // iter of PyList
    var L = PyList(["L", "i", "s", "t"])
    for (l in L) {
        print("${l} ")      // L i s t
    }
    print("\n")

    // iter of PyDict
    var D = PyDict(HashMap<Int64, String>([(1, "D"), (2, "i"), (3, "c"), (4, "t")]))
    for (d in D) {
        print("${d} ")      // 1 2 3 4, dict print keys.
    }
    print("\n")

    // iter of PySet
    var Se = PySet(HashSet<Int64>([1, 2, 3]))
    for (s in Se) {
        print("${s} ")      // 1 2 3
    }
    print("\n")
    0
}

Execution result:

S t r
T u p
L i s t
1 2 3 4
1 2 3

Registration and Callback between Cangjie and Python

The Python interoperability library supports simple function registration and Python's calling of Cangjie functions.

The Python callback code needs to be called using C as the medium, and the third-party libraries of Python, ctypes and _ctypes, are used.

Mapping between Cangjie, C, and Python Types

The following table lists the basic data.

Cangjie TypeCTypePython Type
BoolPyCBoolPyBool
RunePyCWcharPyString
Int8PyCBytePyLong
UInt8PyCUbyte/PyCCharPyLong
Int16PyCShortPyLong
UInt16PyCUshortPyLong
Int32PyCIntPyLong
UInt32PyCUintPyLong
Int64PyCLonglongPyLong
UInt64PyCUlonglongPyLong
Float32PyCFloatPyFloat
Float64PyCDoublePyFloat
[unsupport CPointer as param] CPointer<T>PyCPointerctypes.pointer
[unsupport CString as param] CStringPyCCpointerctypes.c_char_p
[unsupport CString as param] CStringPyCWcpointerctypes.c_wchar_p
UnitPyCVoid-
  • Cangjie Type is a variable type modified on the Cangjie side. Unless otherwise specified, parameters of this type can be transferred to Python code and can be transferred from Python to Cangjie.
  • PyCType is the PyCFunc interface configuration type on the Cangjie side. For details, see Prototype of PyCFunc and Example.
  • Python Type is a type mapping on the Cangjie side. There is no pointer type mapping. Python functions with pointers cannot be called from the Cangjie side.
  • Both PyCCpointer and PyCWcpointer are mapped to CString. The difference is that PyCCpointer is a character string in C, and PyCWcpointer is only a character pointer. Even if multiple characters are transferred, only the first character is used.
  • Type mismatch will result in unpredictable results.

Prototype of PyCFunc Class

PyCFunc is a PyObj subtype based on the Python interoperability library and Python third-party library ctype/_ctype. This type can be directly transferred to Python for use. PyCFunc provides the CFunc function for Python to register with Cangjie and allows Python to call back the CFunc function.

Code prototype:

public enum PyCType {
    PyCBool |
    PyCChar |
    PyCWchar |
    PyCByte |
    PyCUbyte |
    PyCShort |
    PyCUshort |
    PyCInt |
    PyCUint |
    PyCLonglong |
    PyCUlonglong |
    PyCFloat |
    PyCDouble |
    PyCPointer |
    PyCCpointer |
    PyCWcpointer |
    PyCVoid
}

public class PyCFunc <: PyObj {
    public init(f: CPointer<Unit>, argsTy!: Array<PyCType> = [], retTy!: PyCType = PyCType.PyCVoid) { ... }
    public func setArgTypes(args: Array<PyCType>): PyCFunc { ... }
    public func setRetTypes(ret: PyCType): PyCFunc { ... }
}

Description of Class

  • PyCFunc is inherited from PyObj. Some interfaces of the parent class can be used. (If an interface is not supported, an error is reported.)

  • The init can be constructed by external users. The function pointer must be provided as the first parameter (the CFunc type needs to be converted to the CPointer<Unit> type on the Cangjie side). The last two optional parameters are the array of the input parameter type and the return value type.

    If the input pointer is not a function pointer, the program will crash when the function is called (the library layer cannot intercept the function).

  • The setArgTypes/setRetTypes function is used to configure parameters and return value types. For details about the supported parameters, see the PyCType enumeration.

  • The () operator in the parent class can call the registered CFunc function on the Cangjie side.

  • This class can be directly transferred to the Python side or directly called on the Cangjie side. (If a non-function pointer is used during class construction, the system will crash when this class is called.)

  • This class supports chain calling similar to Java Script.

Example

  1. Prepare the CFunc function of Cangjie.
@C
func cfoo(a: Bool, b: Int32, c: Int64): CPointer<Unit> {
    print("cfoo called.\n")
    print("${a}, ${b}, ${c}\n")
    return CPointer<Unit>()
}
  1. Construct a PyCFunc class object.
import std.ffi.python.*

// Define the @C function.
@C
func cfoo(a: Bool, b: Int32, c: Int64): CPointer<Unit> {
    print("cfoo called.\n")
    print("${a}, ${b}, ${c}\n")
    return CPointer<Unit>()
}

main() {
    Python.load()
    /*
    Construct PyCFunc class.
    Set args type:  Bool -> PyCBool
                    Int32 -> PyCInt
                    Int64 -> PyCLonglong
                    CPointer<Unit> -> PyCPointer
    */
    var f1 = PyCFunc(unsafe {CPointer<Unit>(cfoo)},
                    argsTy: [PyCBool, PyCInt, PyCLonglong],
                    retTy: PyCPointer)

    // You also can use it by chain-call.
    var f2 = PyCFunc(unsafe {CPointer<Unit>(cfoo)})
            .setArgTypes([PyCBool, PyCInt, PyCLonglong])
            .setRetTypes(PyCPointer)([true, 1, 2])

    // Call f1
    f1([true, 1, 2])
    f1([PyBool(true), PyLong(1), PyLong(2)])

    Python.unload()
    0
}

Compile and run the following file:

$ cjc ./main.cj -o ./main && ./main
cfoo called.
true, 1, 2
cfoo called.
true, 1, 2
cfoo called.
true, 1, 2
  1. Register the function with Python and enable Python to call the function.

The Python code is as follows:

# File test.py

# `foo` get a function pointer and call it.
def foo(func):
    func(True, 10, 40)

Modify the preceding main.

import std.ffi.python.*

// Define the @C function.
@C
func cfoo(a: Bool, b: Int32, c: Int64): CPointer<Unit> {
    print("cfoo called.\n")
    print("${a}, ${b}, ${c}\n")
    return CPointer<Unit>()
}

main() {
    Python.load()

    var f1 = PyCFunc(unsafe {CPointer<Unit>(cfoo)},
                    argsTy: [PyCBool, PyCInt, PyCLonglong],
                    retTy: PyCPointer)

    // Import test.py
    var cfunc01 = Python.Import("test")

    // Call `foo` and transfer `f1`
    cfunc01["foo"]([f1])

    Python.unload()
    0
}
  1. The Python side transfers the pointer to the Cangjie side.

Add a function to the Python file.

# File test.py

# If you want transfer pointer type to Cangjie CFunc, you need import ctypes.
import ctypes.*

# `foo` get a function pointer and call it.
def foo(func):
    func(True, 10, 40)

# `fooptr` get a function pointer and call it with pointer type args.
def fooptr(func):
    a = c_int(10)
    # c_char_p will get whole symbols, but c_wchar_p only get first one symbol 'd'.
    func(pointer(a), c_char_p(b'abc'), c_wchar_p('def'))

Modify the Cangjie code.

import std.ffi.python.*

var x = Python.load()

// Modify the `foo` param type to pointer.
@C
func foo(a: CPointer<Int64>, b: CString, c: CString): Unit {
    print("${unsafe {a.read(0)}}, ${b.toString()}, ${c.toString()}\n")
}

main(): Int64 {

    var f1 = PyCFunc(unsafe {CPointer<Unit>(foo)},
                    argsTy: [PyCPointer, PyCCpointer, PyCWcpointer],
                    retTy: PyCVoid)

    // Import test.py
    var test = Python.Import("test")

    // Call `fooptr` and transfer `f1`
    test["fooptr"]([f1])
    return 0
}

The pointer type cannot be transferred to the Python library when the function is called on the Cangjie side. Therefore, the function can be called only on the Python side.

Compile and run the following command:

$ cjc ./main.cj -o ./main && ./main
10, abc, d