跨语言互操作

Foreign Function Interfaces (FFI) 是一种机制,通过该机制,一种编程语言写的程序可以调用另外一种编程语言编写的函数。

与 C 语言互操作

为了兼容已有的生态,仓颉支持调用 C 语言的函数,也支持 C 语言调用仓颉的函数。

仓颉调用 C 的函数

在仓颉中要调用 C 的函数,需要在仓颉语言中用 @Cforeign 关键字声明这个函数,但 @C 在修饰 foreign 声明的时候,可以省略。

举个例子,假设我们要调用 C 的 randprintf 函数,它的函数签名是这样的:

// stdlib.h
int rand();

// stdio.h
int printf (const char *fmt, ...);

那么在仓颉中调用这两个函数的方式如下:

// declare the function by `foreign` keyword, and omit `@C`
foreign func rand(): Int32
foreign func printf(fmt: CString, ...): Int32

main() {
    // call this function by `unsafe` block
    let r = unsafe { rand() }
    println("random number ${r}")
    unsafe {
        var fmt = LibC.mallocCString("Hello, No.%d\n")
        printf(fmt, 1)
        LibC.free(fmt)
    }
}

需要注意的是:

  1. foreign 修饰函数声明,代表该函数为外部函数。被 foreign 修饰的函数只能有函数声明,不能有函数实现。
  2. foreign 声明的函数,参数和返回类型必须符合 C 和仓颉数据类型之间的映射关系(详见下节:类型映射)。
  3. 由于 C 侧函数很可能产生不安全操作,所以调用 foreign 修饰的函数需要被 unsafe 块包裹,否则会发生编译错误。
  4. @C 修饰的 foreign 关键字只能用来修饰函数声明,不可用来修饰其他声明,否则会发生编译错误。
  5. @C 只支持修饰 foreign 函数、top-level 作用域中的非泛型函数和 struct 类型。
  6. foreign 函数不支持命名参数和参数默认值。foreign 函数允许变长参数,使用 ... 表达,只能用于参数列表的最后。变长参数均需要满足 CType 约束,但不必是同一类型。
  7. 仓颉虽然提供了栈扩容能力,但是由于 C 侧函数实际使用栈大小仓颉无法感知,所以 ffi 调用进入 C 函数后,仍然存在栈溢出的风险,需要开发者根据实际情况,修改 cjStackSize 的配置。

一些不合法的 foreign 声明的示例代码如下:

foreign func rand(): Int32 { // compiler error
    return 0
}
@C
foreign var a: Int32 = 0 // compiler error
@C
foreign class A{} // compiler error
@C
foreign interface B{} // compiler error

CFunc

仓颉中的 CFunc 指可以被 C 语言代码调用的函数,共有以下三种形式:

  1. @C 修饰的 foreign 函数
  2. @C 修饰的仓颉函数
  3. 类型为 CFunclambda 表达式
    • 与普通的 lambda 表达式不同,CFunc lambda 不能捕获变量。
// Case 1
foreign func free(ptr: CPointer<Int8>): Unit

// Case 2
@C
func callableInC(ptr: CPointer<Int8>) {
    print("This function is defined in Cangjie.")
}

// Case 3
let f1: CFunc<(CPointer<Int8>) -> Unit> = { ptr =>
    print("This function is defined with CFunc lambda.")
}

以上三种形式声明/定义的函数的类型均为 CFunc<(CPointer<Int8>) -> Unit>CFunc 对应 C 语言的函数指针类型。这个类型为泛型类型,其泛型参数表示该 CFunc 入参和返回值类型,使用方式如下:

foreign func atexit(cb: CFunc<() -> Unit>)

foreign 函数一样,其他形式的 CFunc 的参数和返回类型必须满足 CType 约束,且不支持命名参数和参数默认值。

CFunc 在仓颉代码中被调用时,需要处在 unsafe 上下文中。

仓颉语言支持将一个 CPointer<T> 类型的变量类型转换为一个具体的 CFunc,其中 CPointer 的泛型参数 T 可以是满足 CType 约束的任意类型,使用方式如下:

main() {
    var ptr = CPointer<Int8>()
    var f = CFunc<() -> Unit>(ptr)
    unsafe { f() } // core dumped when running, because the pointer is nullptr.
}

注意:将一个指针强制类型转换为 CFunc 并进行函数调用是危险行为,需要用户保证指针指向的是一个切实可用的函数地址,否则将发生运行时错误。

inout 参数

在仓颉中调用 CFunc 时,其实参可以使用 inout 关键字修饰,组成引用传值表达式,此时,该参数按引用传递。引用传值表达式的类型为 CPointer<T>,其中 Tinout 修饰的表达式的类型。

引用传值表达式具有以下约束:

  • 仅可用于对 CFunc 的调用处;
  • 其修饰对象的类型必须满足 CType 约束,但不可以是 CString
  • 其修饰对象不可以是用 let 定义的,不可以是字面量、入参、其他表达式的值等临时变量;
  • 通过仓颉侧引用传值表达式传递到 C 侧的指针,仅保证在函数调用期间有效,即此种场景下 C 侧不应该保存指针以留作后用。

inout 修饰的变量,可以是定义在 top-level 作用域中的变量、局部变量、struct 中的成员变量,但不能直接或间接来源于 class 的实例成员变量。

下面是一个例子:

foreign func foo1(ptr: CPointer<Int32>): Unit

@C
func foo2(ptr: CPointer<Int32>): Unit {
    let n = unsafe { ptr.read() }
    println("*ptr = ${n}")
}

let foo3: CFunc<(CPointer<Int32>) -> Unit> = { ptr =>
    let n = unsafe { ptr.read() }
    println("*ptr = ${n}")
}

struct Data {
    var n: Int32 = 0
}

class A {
    var data = Data()
}

main() {
    var n: Int32 = 0
    unsafe {
        foo1(inout n)  // OK
        foo2(inout n)  // OK
        foo3(inout n)  // OK
    }
    var data = Data()
    var a = A()
    unsafe {
        foo1(inout data.n)   // OK
        foo1(inout a.data.n) // Error, n is derived indirectly from instance member variables of class A
    }
}

**注意:**使用宏扩展特性时,在宏的定义中,暂时不能使用 inout 参数特性。

unsafe

在引入与 C 语言的互操作过程中,同时也引入了 C 的许多不安全因素,因此在仓颉中使用 unsafe 关键字,用于对跨 C 调用的不安全行为进行标识。

关于 unsafe 关键字,有以下几点说明:

  • unsafe 可以修饰函数、表达式,也可以修饰一段作用域。
  • @C 修饰的函数,被调用处需要在 unsafe 上下文中。
  • 在调用 CFunc 时,使用处需要在 unsafe 上下文中。
  • foreign 函数在仓颉中进行调用,被调用处需要在 unsafe 上下文中。
  • 当被调用函数被 unsafe 修饰时,被调用处需要在 unsafe 上下文中。

使用方式如下:

foreign func rand(): Int32

@C
func foo(): Unit {
    println("foo")
}

var foo1: CFunc<() -> Unit> = { =>
    println("foo1")
}

main(): Int64 {
    unsafe {
        rand()           // Call foreign func.
        foo()            // Call @C func.
        foo1()           // Call CFunc var.
    }
    0
}

注意: 普通 lambda 无法传递 unsafe 属性,当 unsafelambda 逃逸后,可以不在 unsafe 上下文中直接调用而未产生任何编译错误。当需要在 lambda 中调用 unsafe 函数时,建议在 unsafe 块中进行调用,参考如下用例:

unsafe func A(){}
unsafe func B(){
    var f = { =>
        unsafe { A() } // Avoid calling A() directly without unsafe in a normal lambda.
    }  
    return f  
}
main() {
    var f = unsafe{ B() }
    f()
    println("Hello World")
}

调用约定

函数调用约定描述调用者和被调用者双方如何进行函数调用(如参数如何传递、栈由谁清理等),函数调用和被调用双方必须使用相同的调用约定才能正常运行。仓颉编程语言通过 @CallingConv 来表示各种调用约定,支持的调用约定如下:

  • CDECL, CDECL 表示 clang 的 C 编译器在不同平台上默认使用的调用约定。
  • STDCALL, STDCALL 表示 Win32 API 使用的调用约定。

通过 C 语言互操作机制调用的 C 函数,未指定调用约定时将采用默认的 CDECL 调用约定。如下调用 C 标准库函数 rand 示例:

@CallingConv[CDECL]   // Can be omitted in default.
foreign func rand(): Int32

main() {
    println(rand())
}

@CallingConv 只能用于修饰 foreign 块、单个 foreign 函数和 top-level 作用域中的 CFunc 函数。当 @CallingConv 修饰 foreign 块时,会为 foreign 块中的每个函数分别加上相同的 @CallingConv 修饰。

类型映射

基础类型

仓颉与 C 语言支持基本数据类型的映射,总体原则是:

  1. 仓颉的类型不包含指向托管内存的引用类型;
  2. 仓颉的类型和 C 的类型具有同样的内存布局。

比如说,一些基本的类型映射关系如下:

Cangjie TypeC TypeSize (byte)
Unitvoid0
Boolbool1
UInt8char1
Int8int8_t1
UInt8uint8_t1
Int16int16_t2
UInt16uint16_t2
Int32int32_t4
UInt32uint32_t4
Int64int64_t8
UInt64uint64_t8
IntNativessize_tplatform dependent
UIntNativesize_tplatform dependent
Float32float4
Float64double8

注:int 类型、long 类型等由于其在不同平台上的不确定性,需要程序员自行指定对应仓颉编程语言类型。在 C 互操作场景中,与 C 语言类似,Unit 类型仅可作为 CFunc 中的返回类型和 CPointer 的泛型参数。

仓颉也支持与 C 语言的结构体和指针类型的映射。

结构体

对于结构体类型,仓颉用 @C 修饰的 struct 来对应。比如说 C 语言里面有这样的一个结构体:

typedef struct {
    long long x;
    long long y;
    long long z;
} Point3D;

那么它对应的仓颉类型可以这么定义:

@C
struct Point3D {
    var x: Int64 = 0
    var y: Int64 = 0
    var z: Int64 = 0
}

如果 C 语言里有这样的一个函数:

Point3D addPoint(Point3D p1, Point3D p2);

那么对应的,在仓颉里面可以这样声明这个函数:

foreign func addPoint(p1: Point3D, p2: Point3D): Point3D

@C 修饰的 struct 必须满足以下限制:

  • 成员变量的类型必须满足 CType 约束
  • 不能实现或者扩展 interfaces
  • 不能作为 enum 的关联值类型
  • 不允许被闭包捕获
  • 不能具有泛型参数

@C 修饰的 struct 自动满足 CType 约束。

指针

对于指针类型,仓颉提供 CPointer<T> 类型来对应 C 侧的指针类型,其泛型参数 T 需要满足 CType 约束。 比如对于 malloc 函数,在 C 里面的签名为:

void* malloc(size_t size);

那么在仓颉中,它可以声明为:

foreign func malloc(size: UIntNative): CPointer<Unit>

CPointer 可以进行读写、偏移计算、判空以及转为指针的整型形式等,详细 API 可以参考 core 包标准库文档。其中读写和偏移计算为不安全行为,当不合法的指针调用这些函数时,可能发生未定义行为,这些 unsafe 函数需要在 unsafe 块中调用。

CPointer 的使用示例如下:

foreign func malloc(size: UIntNative): CPointer<Unit>
foreign func free(ptr: CPointer<Unit>): Unit

@C
struct Point3D {
    var x: Int64
    var y: Int64
    var z: Int64

    init(x: Int64, y: Int64, z: Int64) {
        this.x = x
        this.y = y
        this.z = z
    }
}

main() {
    let p1 = CPointer<Point3D>() // create a CPointer with null value
    if (p1.isNull()) {  // check if the pointer is null
        print("p1 is a null pointer")
    }

    let sizeofPoint3D: UIntNative = 24
    var p2 = unsafe { malloc(sizeofPoint3D) }    // malloc a Point3D in heap
    var p3 = unsafe { CPointer<Point3D>(p2) }    // pointer type cast

    unsafe { p3.write(Point3D(1, 2, 3)) } // write data through pointer

    let p4: Point3D = unsafe { p3.read() } // read data through pointer

    let p5: CPointer<Point3D> = unsafe { p3 + 1 } // offset of pointer

    unsafe { free(p2) }
}

仓颉语言支持 CPointer 之间的强制类型转换,转换前后的 CPointer 的泛型参数 T 均需要满足 CType 的约束,使用方式如下:

main() {
    var pInt8 = CPointer<Int8>()
    var pUInt8 = CPointer<UInt8>(pInt8) // CPointer<Int8> convert to CPointer<UInt8>
    0
}

仓颉语言支持将一个 CFunc 类型的变量类型转换为一个具体的 CPointer,其中 CPointer 的泛型参数 T 可以是满足 CType 约束的任意类型,使用方式如下:

foreign func rand(): Int32
main() {
    var ptr = CPointer<Int8>(rand)
    0
}

注意:将一个 CFunc 强制类型转换为指针通常是安全的,但是不应该对转换后的指针执行任何的 readwrite 操作,可能会导致运行时错误。

数组

仓颉使用 VArray 类型与 C 的数组类型映射,VArray 可以用户作为函数参数和 @C struct 成员。当 VArray<T, $N> 中的元素类型 T 满足 CType 约束时, VArray<T, $N> 类型也满足 CType 约束。

作为函数参数类型:

VArray 作为 CFunc 的参数时, CFunc 的函数签名仅可以是 CPointer<T> 类型或 VArray<T, $N> 类型。当函数签名中的参数类型为 VArray<T, $N> 时,传递的参数仍以 CPointer<T> 形式传递。

VArray 作为参数的使用示例如下:

foreign func cfoo1(a: CPointer<Int32>):Unit
foreign func cfoo2(a: VArray<Int32, $3): Unit

对应的 C 侧函数定义可以是:

void cfoo1(int *a) { ... }
void cfoo2(int a[3]) { ... }

调用 CFunc 时,需要通过 inout 修饰 VArray 类型变量:

var a: VArray<Int32, $3> = [1, 2, 3]
unsafe {
    cfoo1(inout a)
    cfoo2(inout a)
}

VArray 不允许作为 CFunc 的返回值类型。

作为 @C struct 成员:

VArray 作为 @C struct 成员时,它的内存布局与 C 侧的结构体排布一致,需要保证仓颉侧声明长度与类型也与 C 完全一致:

struct S {
    int a[2];
    int b[0];
}

在仓颉中,可以声明为如下结构体与 C 代码对应:

@C
struct S {
    var a = VArray<Int32, $2>(item: 0)
    var b = VArray<Int32, $0>(item: 0)
}

注意:C 语言中允许结构体的最后一个字段为未指明长度的数组类型,该数组被称为柔性数组(flexible array),仓颉不支持包含柔性数组的结构体的映射。

字符串

特别地,对于 C 语言中的字符串类型,仓颉中设计了一个 CString 类型来对应。为简化为 C 语言字符串的操作,CString 提供了以下成员函数:

  • init(p: CPointer<UInt8>) 通过 CPointer 构造一个 CString
  • func getChars() 获取字符串的地址,类型为 CPointer<UInt8>
  • func size(): Int64 计算该字符串的长度
  • func isEmpty(): Bool 判断该字符串的长度是否为 0,如果字符串的指针为空返回 true
  • func isNotEmpty(): Bool 判断该字符串的长度是否不为 0,如果字符串的指针为空返回 false
  • func isNull(): Bool 判断该字符串的指针是否为 null
  • func startsWith(str: CString): Bool 判断该字符串是否以 str 开头
  • func endsWith(str: CString): Bool 判断该字符串是否以 str 结尾
  • func equals(rhs: CString): Bool 判断该字符串是否与 rhs 相等
  • func equalsLower(rhs: CString): Bool 判断该字符串是否与 rhs 相等,忽略大小写
  • func subCString(start: UInt64): CString 从 start 开始截取子串,返回的子串存储在新分配的空间中
  • func subCString(start: UInt64, len: UInt64): CString 从 start 开始截取长度为 len 的子串,返回的子串存储在新分配的空间中
  • func compare(str: CString): Int32 该字符串与 str 比较,返回结果与 C 语言的 strcmp(this, str) 一样
  • func toString(): String 用该字符串构造一个新的 String 对象
  • func asResource(): CStringResource 获取 CString 的 Resource 类型

另外,将 String 类型转换为 CString 类型,可以通过调用 LibC 中的 mallocCString 接口,使用完成后需要对 CString 进行释放。

CString 的使用示例如下:

foreign func strlen(s: CString): UIntNative

main() {
    var s1 = unsafe { LibC.mallocCString("hello") }
    var s2 = unsafe { LibC.mallocCString("world") }

    let t1: Int64 = s1.size()
    let t2: Bool = s2.isEmpty()
    let t3: Bool = s1.equals(s2)
    let t4: Bool = s1.startsWith(s2)
    let t5: Int32 = s1.compare(s2)

    let length = unsafe { strlen(s1) }

    unsafe {
        LibC.free(s1)
        LibC.free(s2)
    }
}

sizeOf/alignOf

在仓颉标准库中,提供了 sizeOfalignOf 两个函数,用于获取上述 C 互操作类型的内存占用和内存对齐数值(单位:字节),函数声明如下:

public func sizeOf<T>(): UIntNative where T <: CType
public func alignOf<T>(): UIntNative where T <: CType

使用示例:

@C
struct Data {
    var a: Int64 = 0
    var b: Float32 = 0.0
}

main() {
    println(sizeOf<Data>())
    println(alignOf<Data>())
}

在 64 位机器上运行,将输出:

16
8

CType

除类型映射一节提供的与 C 侧类型进行映射的类型外,仓颉还提供了一个 CType 接口,接口本身不包含任何方法,它可以作为所有 C 互操作支持的类型的父类型,便于在泛型约束中使用。

需要注意的是:

  1. CType 接口是仓颉中的一个接口类型,它本身不满足 CType 约束;
  2. CType 接口不允许被继承、扩展;
  3. CType 接口不会突破子类型的使用限制。

CType 的使用示例如下:

func foo<T>(x: T): Unit where T <: CType {
    match (x) {
        case i32: Int32 => println(i32)
        case ptr: CPointer<Int8> => println(ptr.isNull())
        case f: CFunc<() -> Unit> => unsafe { f() }
        case _ => println("match failed")
    }
}

main() {
    var i32: Int32 = 1
    var ptr = CPointer<Int8>()
    var f: CFunc<() -> Unit> = { => println("Hello") }
    var f64 = 1.0
    foo(i32)
    foo(ptr)
    foo(f)
    foo(f64)
}

执行结果如下:

1
true
Hello
match failed

C 调用仓颉的函数

仓颉提供 CFunc 类型来对应 C 侧的函数指针类型。C 侧的函数指针可以传递到仓颉,仓颉也可以构造出对应 C 的函数指针的变量传递到 C 侧。

假设一个 C 的库 API 如下:

typedef void (*callback)(int);
void set_callback(callback cb);

对应的,在仓颉里面这个函数可以声明为:

foreign func set_callback(cb: CFunc<(Int32) -> Unit>): Unit

CFunc 类型的变量可以从 C 侧传递过来,也可以在仓颉侧构造出来。在仓颉侧构造 CFunc 类型有两种办法,一个是用 @C 修饰的函数,另外一个是标记为 CFunc 类型的闭包。

@C 修饰的函数,表明它的函数签名是满足 C 的调用规则的,定义还是写在仓颉这边。foreign 修饰的函数定义是在 C 侧的。

注意:foreign 修饰的函数与 @C 修饰的函数,这两种 CFunc 的命名不建议使用 CJ_(不区分大小写)作为前缀,否则可能与标准库及运行时等编译器内部符号出现冲突,导致未定义行为。

示例如下:

@C
func myCallback(s: Int32): Unit {
    println("handle ${s} in callback")
}

main() {
    // the argument is a function qualified by `@C`
    unsafe { set_callback(myCallback) }

    // the argument is a lambda with `CFunc` type
    let f: CFunc<(Int32) -> Unit> = { i => "handle ${i} in callback" }
    unsafe { set_callback(f) }
}

假设 C 函数编译出来的库是 "libmyfunc.so",那么需要使用 cjc -L. -lmyfunc test.cj -o test.out 编译命令,使仓颉编译器去链接这个库。最终就能生成想要的可执行程序。

另外,在编译 C 代码时,请打开 -fstack-protector-all/-fstack-protector-strong 栈保护选项,仓颉侧代码默认拥有溢出检查与栈保护功能。在引入 C 代码后,需要同步保证 unsafe 块中的溢出的安全性。

编译选项

使用 C 互操作通常需要手动链接 C 的库,仓颉编译器提供了相应的编译选项。

  • --library-path <value>, -L <value>, -L<value>:指定要链接的库文件所在的目录。

    --library-path <value> 指定的路径会被加入链接器的库文件搜索路径。另外环境变量 LIBRARY_PATH 中指定的路径也会被加入链接器的库文件搜索路径中,通过 --library-path 指定的路径会比 LIBRARY_PATH 中的路径拥有更高的优先级。

  • --library <value>, -l <value>, -l<value>:指定要链接的库文件。

    给定的库文件会被直接传给链接器,库文件名的格式应为 lib[arg].[extension]

关于仓颉编译器支持的所有编译选项,详见仓颉编译器手册一章。

示例

假设我们有一个 C 库 libpaint.so,其头文件如下:

#include <stdint.h>

typedef struct {
    int64_t x;
    int64_t y;
} Point;

typedef struct {
    int64_t x;
    int64_t y;
    int64_t r;
} Circle;

int32_t DrawPoint(const Point* point);
int32_t DrawCircle(const Circle* circle);

在仓颉代码中使用该 C 库的示例代码如下:

// main.cj
foreign {
    func DrawPoint(point: CPointer<Point>): Int32
    func DrawCircle(circle: CPointer<Circle>): Int32

    func malloc(size: UIntNative): CPointer<Int8>
    func free(ptr: CPointer<Int8>): Unit
}

@C
struct Point {
    var x: Int64 = 0
    var y: Int64 = 0
}

@C
struct Circle {
    var x: Int64 = 0
    var y: Int64 = 0
    var r: Int64 = 0
}

main() {
    let SIZE_OF_POINT: UIntNative = 16
    let SIZE_OF_CIRCLE: UIntNative = 24
    let ptr1 = unsafe { malloc(SIZE_OF_POINT) }
    let ptr2 = unsafe { malloc(SIZE_OF_CIRCLE) }

    let pPoint = CPointer<Point>(ptr1)
    let pCircle = CPointer<Circle>(ptr2)

    var point = Point()
    point.x = 10
    point.y = 20
    unsafe { pPoint.write(point) }

    var circle = Circle()
    circle.r = 1
    unsafe { pCircle.write(circle) }

    unsafe {
        DrawPoint(pPoint)
        DrawCircle(pCircle)

        free(ptr1)
        free(ptr2)
    }
}

编译仓颉代码的命令如下:

cjc -L . -l paint ./main.cj

其中编译命令中 -L . 表示链接库时从当前目录查找(假设 libpaint.so 存在于当前目录),-l paint 表示链接的库的名字,编译成功后默认生成二进制文件 main,执行二进制文件的命令如下:

LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH ./main

与 Python 语言互操作

为了兼容强大的计算和 AI 生态,仓颉支持与 Python 语言的互操作调用。Python 的互操作通过 std 模块中的 ffi.python 库为用户提供能力。

目前 Python 互操作仅支持在 Linux 平台使用,并且仅支持仓颉编译器的 llvmgc 后端。

Python 的全局资源及使用

提供内建函数类以及全局资源

代码原型:

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

Python 库提供的接口不能保证并发安全,当对 Python 进行异步调用时(系统线程 ID 不一致)会抛出 PythonException 异常。

在 Python 初始化时,GIL 全局解释器锁基于当前所在 OS 线程被锁定,如果执行的代码所在的 Cangjie 线程(包括 main 所在 Cangjie 线程)在 OS 线程上发生调度(OS 线程 ID 发生变化),Python 内部再次尝试检查 GIL 时会对线程状态进行校验,发现 GIL 状态中保存的 OS 线程 ID 与当前执行的 OS 线程 ID 不一致,此时会触发内部错误,导致程序崩溃。

**注意:**由于 Python 互操作使用到大量 Python 库的 native 代码,这部分代码在仓颉侧无法对其进行相应的栈保护。仓颉栈保护默认大小为 64KB,在对 Python C API 进行调用过程中,容易造成 native 代码超出默认栈大小,发生溢出,会触发不可预期的结果。建议用户在执行 Python 互操作相关代码前,配置仓颉默认栈大小至少为 1MB:export cjStackSize=1MB

使用示例:

from std import ffi.python.*

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

提供 Python 库日志类 PythonLogger

代码原型:

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()

Logger 类的几点声明:

  • PythonLogger 实现 Logger 接口仅做打印输出以及打印等级控制,不做日志转储到 log 文件;
  • setOutput 为空实现,不支持 log 转储文件;
  • info/warn/error 等接口输出打印以对应前缀开头,其他不做区分;
  • PythonLogger 默认打印等级为 LogLevel.WARN
  • PYLOG.error(msg)log(LogLevel.ERROR, msg) 接口会抛出 PythonException 异常。

使用示例:

from std import ffi.python.*
from std import 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
}

执行结果:

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

提供 Python 库异常类 PythonException

代码原型:

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

PythonException 有以下说明:

  • PythonException 与被继承的 Exception 除了异常前缀存在差异,其他使用无差异;
  • 当 Python 内部出现异常时,外部可以通过 try-catch 进行捕获,如果不进行捕获会打印异常堆栈并退出程序,返回值为 1。

使用示例:

from std import ffi.python.*
from std import 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
}

提供 Python 库的版本信息类 Version

代码原型:

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
}

关于 Version 类的几点声明:

  • Version 版本信息包含三个部分:major versionminor versionmicro version
  • Version 版本仅通过构造函数进行初始化,一旦定义,后续无法修改。
  • 提供 toString 接口,可以直接进行打印。
  • 提供 getVersion 接口,可以获取版本的 tuple 形式。

使用示例:

from std import ffi.python.*

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

PythonBuiltins 内建函数类

Python 库的导入和加载

代码原型:

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()

关于加载与卸载有以下几点声明:

  • load 函数使用重载的方式实现,同时支持无参加载和指定动态库路径加载,提供可选参数配置 PythonLogger 的打印等级,如果不配置,会将 PYLOG 重置为 warn 打印等级;
  • load() 函数进行了 Python 相关的准备工作,在进行 Python 互操作前必须调用,其中动态库查询方式请见:动态库的加载策略;
  • load(path: String) 函数需要用户配置动态库路径 pathpath 指定到动态库文件(如:/usr/lib/libpython3.9.so),不可以配置为目录或者非动态库文件;
  • load 函数失败时会抛出 PythonException 异常,如果程序仍然需要继续执行,请注意 try-catch
  • unload 函数在进行完 Python 互操作时调用,否则会造成相关资源泄露;
  • 加载和卸载操作仅需要调用一次,并且一一对应,多次调用仅第一次生效;
  • isload() 函数用于判断 Python 库是否被加载。

使用示例:

loadunload

from std import ffi.python.*

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

isLoad 函数:

from std import ffi.python.*

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

动态库的加载策略

Python 库需要依赖 Python 的官方动态链接库: libpython3.x.so ,推荐版本:3.9.2,支持读取 Python3.0 以上版本。

从 Python 源码编译获取动态库:

# 在Python源码路径下:
$ ./configure --enable-shared --with-system-ffi --prefix=/usr
$ make
$ make install

Python 的动态库按照以下方式进行自动查找:

1、使用指定的环境变量:

$ export PYTHON_DYNLIB=".../libpython3.9.so"

2、如果环境变量未指定,从可执行文件的依赖中查找:

  • 需要保证可执行文件 python3 可正常执行(所在路径已添加值 PATH 环境变量中),通过对 python3 可执行文件的动态库依赖进行查询。
  • 非动态库依赖的 Python 可执行文件无法使用(源码编译未使用 --enable-shared 编译的 Python 可执行文件,不会对动态库依赖)。
$ ldd $(which python3)
    ...
    libpython3.9d.so.1.0 => /usr/local/lib/libpython3.9d.so.1.0 (0x00007f499102f000)
    ...

3、如果无法找到可执行文件依赖,尝试从系统默认动态库查询路径中查找:

["/lib", "/usr/lib", "/usr/local/lib"]
  • 所在路径下查询的动态库名称必须满足 libpythonX.Y.so 的命名方式,其中 X Y 分别为主版本号以及次版本号,并且支持的后缀有:d.som.sodm.so.so,支持的版本高于 python3.0,低于或等于 python3.10。如:
libpython3.9.so
libpython3.9d.so
libpython3.9m.so
libpython3.9dm.so

使用示例:

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

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

可以开启 Python 的 INFO 级打印,查看 Python 库路径的搜索过程:

# 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() 函数

函数原型:

public func getVersion(): Version

接口描述:

  • getVersion() 函数用于获取当前使用的 Python 版本;

入参返回值:

  • getVersion() 函数无参数,返回 Version 类对象;

异常情况:

  • getVersion() 函数需要保证 load 函数已被调用,否则返回的版本信息号为 0.0.0

使用示例:

from std import ffi.python.*

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

Import() 函数

函数原型:

public func Import(module: String): PyModule

入参返回值:

  • Import 函数接受一个 String 类型入参,即模块名,并且返回一个 PyModule 类型的对象;

异常情况:

  • Import 函数需要保证 load 函数已被调用,否则返回的 PyModule 类型对象不可用( isAvaliable()false );
  • 如果找不到对应的模块,仅会报错,且返回的 PyModule 类型对象不可用( isAvaliable()false )。

使用示例:

from std import 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
}

执行结果:

Import sys success
Import test success
Import test failed

Eval() 函数

函数原型:

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

接口描述:

  • Eval() 函数用于创建一个 Python 数据类型;

入参返回值:

  • Eval() 接受一个 String 类型的命令 cmd ,并返回该指令的结果的 PyObj 形式;
  • Eval() 接受一个 String 类型的指定域,默认域为 "__main__"

异常情况:

  • Eval() 接口需要保证 load 函数已被调用,否则返回的 PyObj 类型对象不可用( isAvaliable()false );
  • Eval() 如果接收的命令执行失败,Python 侧会进行报错,并且返回的 PyObj 类型对象不可用( isAvaliable()false )。

使用示例:

from std import 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
}

执行结果:

123
b is unavailable.

index [] 运算符重载

接口描述:

  • [] 函数提供了其他 Python 的内置函数调用能力;

入参返回值:

  • [] 函数入参接受 String 类型的内建函数名,返回类型为 PyObj

异常处理:

  • [] 函数需要保证 load 函数已被调用,否则返回的 PyObj 类型对象不可用( isAvaliable()false );
  • 如果指定的函数名未找到,则会报错,且返回的 PyObj 类型对象不可用( isAvaliable()false )。

使用示例:

from std import 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
}

执行结果:

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

类型映射

由于 Python 与仓颉互操作基于 C API 开发,Python 与 C 的数据类型映射统一通过 PyObject 结构体指针完成,并且具有针对不同数据类型的一系列接口。对比 C 语言,仓颉具有面向对象的编程优势,因此将 PyObject 结构体指针统一封装为父类,并且被不同的数据类型进行继承。

类型映射表

仓颉类型到 Python 类型映射

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

Python 类型到仓颉类型映射

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

Python FFI 库泛型约束的接口 PyFFIType

public interface PyFFIType { }
  • 由于部分类引入了泛型,为了对用户在泛型使用过程中进行约束,引入了抽象接口 PyFFIType
  • 该接口无抽象成员函数,其仅被 PyObjCjObj 实现或继承,该接口不允许在包外进行实现,如果用户自定义类并实现改接口,可能发生未定义行为。

PyObj

与 Python 库中的结构体 PyObject 对应,对外提供细分数据类型通用的接口,如成员变量访问、函数访问、到仓颉字符串转换等。

类原型:

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 { ... }
}

关于 PyObj 类的几点说明

  • PyObj 不对外提供创建的构造函数,该类不能在包外进行继承,如果用户自定义类并实现改接口,可能发生未定义行为;

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

    • isAvailable 接口用于判断该 PyObj 是否可用(即封装的 C 指针是否为 NULL)。
  • public open operator func [](key: String): PyObj { ... }

    • [](key) 用于访问 Python 类的成员或者模块中的成员等;
    • 如果 PyObj 本身不可用( isAvaliable()false ),将抛出异常;
    • 如果 PyObj 中不存在对应的 key ,此时由 Python 侧打印对应的错误,并返回不可用的 PyObj 类对象( isAvaliable()false )。
  • public open operator func [](key: String, value!: PyObj): Unit { ... }

    • [](key, value) 设置 Python 类、模块的成员变量值为 value
    • 如果 PyObj 本身不可用( isAvaliable()false ),将抛出异常;
    • 如果 PyObj 中不存在对应的 key ,此时由 Python 侧打印对应的错误;
    • 如果 value 值为一个不可用的对象( isAvaliable()false ),此时会将对应的 key 从模块或类中删除。
  • () 括号运算符重载,可调用对象的函数调用:

    • 如果 PyObj 本身不可用( isAvaliable()false ),将抛出异常;
    • 如果 PyObj 本身为不可调用对象,将由 Python 侧报错,且返回不可用的 PyObj 类对象( isAvaliable()false );
    • () 接受无参的函数调用;
    • ([...]) 接受大于等于 1 个参数传递,参数类型支持仓颉类型 CjObj 和 Python 数据类型 PyObj ,需要注意的是,多个参数传递时,CjObjPyObj 不可混用;
    • 如果参数中包含不可用对象( isAvaliable()false ),此时将会抛出异常,避免发生在 Python 侧出现不可预测的程序崩溃;
    • () 运算符支持 kargs ,即对应 Python 的可变命名参数设计,其通过一个 HashMap 进行传递,其 key 类型 String 配置为变量名, value 类型为 PyObj 配置为参数值。
  • 二元运算符重载:

    • + 两变量相加:

      • 基础数据类型:PyStringPyBool/PyLong/PyFloat 不支持相加,其他类型均可相互相加;
      • 高级数据类型:PyDict/PySet 与所有类型均不支持相加,PyTuple/PyList 仅能与自身相加。
    • - 两变量相减:

      • 基础数据类型:PyStringPyBool/PyLong/PyFloat/PyString 不支持相减,其他类型均可相互相减;
      • 高级数据类型:PyDict/PySet/PyTuple/PyList 与所有类型均不支持相减。
    • * 两变量相乘:

      • 基础数据类型:PyStringPyFloat/PyString 不支持相乘,其他类型均可相乘;
      • 高级数据类型:PyDict/PySet 与所有类型均不支持相乘,PyTuple/PyList 仅能与 PyLong/PyBool 相乘。
    • / 两变量相除:

      • 基础数据类型:PyStringPyBool/PyLong/PyFloat/PyString 不支持相除,其他类型均可相互相除;如果除数为 0(False 在 Python 侧解释为 0,不可作为除数),会在 Python 侧进行错误打印;
      • 高级数据类型:PyDict/PySet/PyTuple/PyList 与所有类型均不支持相除。
    • ** 指数运算:

      • 基础数据类型:PyStringPyBool/PyLong/PyFloat/PyString 不支持指数运算,其他类型均可进行指数运算;
      • 高级数据类型:PyDict/PySet/PyTuple/PyList 与所有类型均不支持指数运算。
    • % 取余:

      • 基础数据类型:PyStringPyBool/PyLong/PyFloat/PyString 不支持取余运算,其他类型均可进行取余运算;如果除数为 0(False 在 Python 侧解释为 0,不可作为除数),会在 Python 侧进行错误打印;
      • 高级数据类型:PyDict/PySet/PyTuple/PyList 与所有类型均不支持取余运算。
    • 以上所有错误情况均会进行 warn 级别打印,并且返回的 PyObj 不可用(isAvaliable()false)。

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

    • toString 函数可以将 Python 数据类型以字符串形式返回,基础数据类型将以 Python 风格返回;
    • 如果 PyObj 本身不可用( isAvaliable()false ),将抛出异常。
  • hashCode 函数为封装的 Python hash 算法,其返回一个 Int64 的哈希值;

  • == 操作符用于判定两个 PyObj 对象是否相同,!= 与之相反,如果接口比较失败,== 返回为 false 并捕获 Python 侧报错,如果被比较的两个对象存在不可用,会抛出异常。

使用示例:

test01.py 文件:

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

同级目录下的仓颉文件 main.cj:

from std import ffi.python.*
from std import 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 接口

接口原型及类型扩展:

public interface CjObj <: PyFFIType {
    func toPyObj(): PyObj
}
extend Bool <: CjObj {
    public func toPyObj(): PyBool { ... }
}
extend Char <: 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 Array<T> <: CjObj where T <: PyFFIType {
    public func toPyObj(): PyList<T> { ... }
}
extend HashMap<K, V> <: CjObj where K <: Hashable & Equatable<K> & PyFFIType {
    public func toPyObj(): PyDict<K, V> { ... }
}
extend HashSet<T> <: CjObj where T <: Hashable, T <: Equatable<T> & PyFFIType {
    public func toPyObj(): PySet<T> { ... }
}

关于 CjObj 类的几点说明

  • CjObj 接口被所有基础数据类型实现并完成 toPyObj 扩展,分别支持转换为与之对应的 Python 数据类型。

PyBoolBool 的映射

类原型:

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

关于 PyBool 类的几点说明

  • PyBool 类继承自 PyObj 类, PyBool 具有所有父类拥有的接口;
  • PyBool 仅允许用户使用仓颉的 Bool 类型进行构造;
  • toCjObj 接口将 PyBool 转换为仓颉数据类型 Bool

使用示例:

from std import 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
}

执行效果:

True
success
b is PyBool

PyLong 与整型的映射

类原型:

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 { ... }
}

关于 PyLong 类的几点说明

  • PyLong 类继承自 PyObj 类, PyLong 具有所有父类拥有的接口;

  • PyLong 支持来自所有仓颉整数类型的入参构造;

  • toCjObjtoInt64 接口将 PyLong 转换为 Int64 类型;

  • toUInt64 接口将 PyLong 转换为 UInt64 类型;

  • PyLong 类型向仓颉类型转换统一转换为 8 字节类型,不支持转换为更低字节类型;

  • 溢出问题:

    • toInt64 原数值(以 UInt64 赋值,赋值不报错)超出 Int64 范围判定为溢出;
    • toUInt64 原数值(以 Int64 赋值,赋值不报错)超出 UInt64 范围判定为溢出;
  • PyLong 暂不支持大数处理。

使用示例:

from std import 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
}

执行效果:

10
success
b is PyLong

PyFloat 与浮点的映射

类原型:

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

关于 PyFloat 类的几点说明

  • PyFloat 类继承自 PyObj 类, PyFloat 具有所有父类拥有的接口;
  • PyBool 支持使用仓颉 Float32/Float64 类型的数据进行构造;
  • toCjObj 接口为了保证精度,将 PyFloat 转换为仓颉数据类型 Float64

使用示例:

from std import 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
}

执行效果:

3.14
success
b is PyFloat

PyString 与字符、字符串的映射

类原型:

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

关于 PyString 类的几点说明

  • PyString 类继承自 PyObj 类, PyString 具有所有父类拥有的接口;
  • PyString 支持使用仓颉 Char/String 类型的数据进行构造;
  • toCjObj/toString 接口为将 PyString 转换为仓颉数据类型 String

使用示例:

from std import 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
}

执行结果:

hello python
success
b is PyString

PyTuple 类型

类原型:

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 { ... }
}

关于 PyTuple 类的几点说明

  • PyTuple 与 Python 中的元组类型一致,即 Python 代码中使用 (...) 的变量;

  • PyTuple 类继承自 PyObj 类, PyTuple 具有所有父类拥有的接口;

  • PyTuple 支持使用仓颉 Array 来进行构造, Array 的元素类型必须为 PyObj (Python 不同数据类型均可以使用 PyObj 传递,即兼容 Tuple 中不同元素的不同数据类型),当成员中包含不可用对象时,会抛出异常;

  • [] 操作符重载:

    • 父类 PyObj[] 入参类型为 String 类型,该类对象调用时能够访问或设置 Python 元组类型内部成员变量或者函数;
    • 子类 PyTuple 支持使用 [] 对元素进行访问,如果角标 key 超出 [0, size()) 区间,会进行报错,并且返回不可用的 PyObj 对象;
    • 由于 Python 的元组为不可变对象,未进行 set [] 操作符重载。
  • size 函数用于获取 PyTuple 的长度;

  • slice 函数用于对源 PyTuple 进行剪裁,并返回一个新的 PyTuple , 如果 slice 的入参 beginend 不在 [0, size()) 区间内,仍会正常裁切。

使用示例:

from std import 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
}

执行结果:

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

PyListArray 的映射

类原型:

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> { ... }
}

关于 PyList 类的几点说明

  • PyList 类与 Python 中的列表类型一致,即 Python 代码中使用 [...] 的变量;

  • PyList 类继承自 PyObj 类, PyList 具有所有父类拥有的接口,该类由于对仓颉的 Array 进行映射,因此该类引入了泛型 TT 类型约束为 PyFFIType 接口的子类;

  • PyList 类可以通过仓颉的 Array 类型进行构造, Array 的成员类型同样约束为 PyFFIType 接口的子类;

  • [] 操作符重载:

    • 父类 PyObj[] 入参类型为 String 类型,该类对象调用时仅能访问或设置 Python 内部成员变量或者函数;
    • 该类中的 [] 入参类型为 Int64 ,即对应 Array 的角标值,其范围为 [0, size()),如果入参不在范围内,将进行报错,并且返回的对象为不可用;
    • [] 同样支持 get 以及 set ,并且 set 时, value 类型为 T ,如果 value 其中包含不可用的 Python 对象时,会抛出异常。
  • toCjObj 函数支持将 PyList 转换为仓颉的 Array<PyObj>,请注意,此时并不会转换为 Array<T>

  • size 函数返回 PyList 的长度;

  • insert 函数将在 index 位置插入 value ,其后元素往后移,index 不在 [0, size()) 可以正常插入,如果 value 为不可用对象,将会抛出异常;

  • append 函数将 item 追加在 PyList 最后,如果 value 为不可用对象,将会抛出异常;

  • slice 函数用于截取 [begin, end) 区间内的数据并且返回一个新的 PyList , beginend 不在 [0, size()) 也可以正常截取。

使用示例:

from std import 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
}

执行结果:

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

PyDictHashMap 的映射

类原型:

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> { ... }
}

关于 PyDict 类的几点说明

  • PyDict 与 Python 的字典类型一致,即 Python 代码中使用 { a: b } 的变量;

  • PyDict 类继承自 PyObj 类, PyDict 具有所有父类拥有的接口,该类由于对仓颉的 HashMap 进行映射,因此该类引入了泛型 <K, V> ,其中 K 类型约束为 PyFFIType 接口的子类,且可被 Hash 计算以及重载了 ==!= 运算符;

  • PyDict 接受来自仓颉类型 HashMap 的数据进行构造:

    • K 仅接受 CjObjPyObj 类型或其子类;
    • 相同的 Python 数据其值也相同,例如 Python.Eval("1")1.toPyObj()== 关系。
  • getItem 函数用于获取 PyDict 对应键值的 value ,如果键值无法找到,会进行报错并返回不可用的 PyObj ,如果配置的值 key 或为 valuePyObj 类型且不可用,此时抛出异常;;

  • setItem 函数用于配置 PyDict 对应键值的 value ,如果对应键值无法找到,会进行插入,如果配置的值 key 或为 valuePyObj 类型且不可用,此时抛出异常;

  • toCjObj 函数用于将 PyDict 转换为 HashMap<PyObj, PyObj> 类型;

  • contains 函数用于判断 key 值是否包含在当前字典中,返回类型为 Bool 型,如果接口失败,进行报错,并且返回 false;

  • copy 函数用于拷贝当前字典,并返回一个新的 PyDict<T> 类型,如果拷贝失败,返回的 PyDict 不可用;

  • del 函数用于删除对应 key 的值,如果 key 值为 PyObj 类型且不可用,会抛出异常;

  • size 函数用于返回当前字典的长度;

  • empty 函数用于清空当前字典内容;

  • items 函数用于获取一个 Python list 类型的键值对列表,可以被迭代访问;

  • values 函数用于获取一个 Python list 类型的值列表,可以被迭代访问;

  • keys 函数用于获取一个 Python list 类型的键列表,可以被迭代访问。

使用示例:

from std import ffi.python.*
from std import 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()
}

PySetHashSet 的映射

类原型:

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 { ... }
}

关于 PySet 类的几点说明

  • PySet 对应的是 Python 中的集合的数据类型,当元素插入时会使用 Python 内部的 hash 算法对集合元素进行排序(并不一定按照严格升序,一些方法可能因此每次运行结果不一致)。

  • PySet 类继承自 PyObj 类, PySet 具有所有父类拥有的接口,该类由于对仓颉的 HashSet 进行映射,因此该类引入了泛型 TT 类型约束为 PyFFIType 接口的子类,且可被 Hash 计算以及重载了 ==!= 运算符;

  • PySet 接受来自仓颉类型 HashMap 的数据进行构造:

    • K 仅接受 CjObjPyObj 类型或其子类;
    • 相同的 Python 数据其值也相同,例如 Python.Eval("1")1.toPyObj()== 关系。
  • toCjObj 函数用于将 PySet<T> 转为 HashSet<PyObj> 需要注意的是此处只能转为元素类型为 PyObj 类型;

  • contains 函数用于判断 key 是否在当前字典中存在, key 类型为 T

  • add 函数可以进行值插入,当 PySet 中已存在键值,则插入不生效,如果 keyPyObj 且不可用,则会抛出异常;

  • pop 函数将 PySet 中的第一个元素取出;

  • del 删除对应的键值,如果 key 不在 PySet 中,则会报错并正常退出,如果 keyPyObj 且不可用,则会抛出异常;

  • size 用于返回 PySet 的长度;

  • empty 用于清空当前 PySet

注意:调用 toCjObj 完后,所有元素将被 pop 出来,此时原 PySet 将会为空( size 为 0,原 PySet 仍然可用);

使用示例:

from std import ffi.python.*
from std import 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 类型

PySlice 类型与 Python 内建函数 slice() 的返回值用法一致,可以被用来标识一段区间及步长,可以用来作为可被切片的类型下标值来剪裁获取子串。为了方便从仓颉侧构造, PySlice 类可以与仓颉的 Range 区间类型进行互相转换,详细描述见以下。

类原型:

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

关于 PySlice 的几点说明:

  • PySlice 可以使用仓颉的 Range 类型来进行构造,并且支持 Range 的语法糖,其中泛型 T 在原有 Range 约束的同时,加上约束在来自 CjObj 的实现,不支持 PyObj 类型;
  • toCjObj 函数支持将 PySlice 转为仓颉 Range 的接口,应注意此时 Range 的泛型类型为 Int64 类型的整型;
  • 如果希望把 PySlice 类型传递给 PyString/PyList/PyTuple 或者是其他可被 slicePyObj 类型,可以通过其成员函数 __getitem__ 进行传递,详情见示例。

使用示例:

from std import 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
}

执行结果:

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

PyObj 的迭代器类型 PyObjIterator

代码原型:

PyObj 的扩展:

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

PyObjIterator 类型:

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

关于 PyObjIterator 的几点说明:

  • 获取 PyObjIterator 可以通过 PyObj 的 iterator 方法获取;

  • PyObjIterator 允许被外部构造,如果提供的 PyObj 不可以被迭代或提供的 PyObj 不可用,则会直接抛出异常;

    • 可以被迭代的对象有:PyString/PyTuple/PyList/PySet/PyDict
    • 注意,直接对 PyDict 进行迭代时,迭代的为其键 key 的值。
  • next 函数用于对该迭代器进行迭代;

  • iterator 方法用于返回本身。

使用示例:

from std import ffi.python.*
from std import 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
}

执行结果:

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

仓颉与 Python 的注册回调

Python 互操作库支持简单的函数注册及 Python 对仓颉函数调用。

Python 回调仓颉代码通过需要通过 C 作为介质进行调用,并且使用到了 Python 的三方库: ctypes 以及 _ctypes

类型映射

基础数据对照如下表:

Cangjie TypeCTypePython Type
BoolPyCBoolPyBool
CharPyCWcharPyString
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 是在仓颉侧修饰的变量类型,无特殊说明则支持传递该类型参数给 Python 代码,并且支持从 Python 传递给仓颉;
  • PyCType 为仓颉侧对应的 PyCFunc 接口配置类型,详细见类原型以及示例展示;
  • Python Type 是在仓颉侧的类型映射,无指针类型映射,不支持从仓颉侧调用 Python 带有指针的函数;
  • PyCCpointerPyCWcpointer 同样都是映射到 CString ,两者区别为 PyCCpointer 为 C 中的字符串, PyCWcpointer 仅为字符指针,即使传递多个字符,也只取第一个字符;
  • 类型不匹配将会导致不可预测的结果。

PyCFunc 类原型

PyCFunc 是基于 Python 互操作库和 Python 三方库 ctype/_ctype 的一个 PyObj 子类型,该类型可以直接传递给 Python 侧使用。 PyCFunc 为用户提供了注册仓颉的 CFunc 函数给 Python 侧,并且支持由 Python 回调 CFunc 函数的能力。

代码原型:

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 { ... }
}

关于类的几点说明:

  • PyCFunc 继承自 PyObj ,可以使用父类的部分接口(如果不支持的接口会相应报错);

  • init 允许外部用户构造,必须提供函数指针作为第一个参数(仓颉侧需要将 CFunc 类型转换为 CPointer<Unit> 类型),后面两个可选参数分别为入参类型的数组、返回值类型;

    这里特别声明,如果传入的指针并非函数指针会导致函数调用时程序崩溃(库层面无法进行拦截);

  • setArgTypes/setRetTypes 函数用于配置参数和返回值类型,支持的参数见 PyCType 枚举;

  • 父类中的 () 操作符,支持在仓颉侧调用该注册的 CFunc 函数;

  • 该类可以直接传递给 Python 侧使用,也可以在仓颉侧直接调用(如果该类构造时使用非函数指针,这里调用将会崩溃);

  • 该类支持类似 Js 的链式调用。

示例

1、准备仓颉的 CFunc 函数:

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

2、构造 PyCFunc 类对象:

from std import 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
}

编译仓颉文件并执行:

$ cjc ./main.cj -o ./main && ./main
cfoo called.
true, 1, 2
cfoo called.
true, 1, 2
cfoo called.
true, 1, 2

3、将函数注册给 Python 并且由 Python 进行调用:

Python 代码如下:

# File test.py

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

对上面仓颉 main 进行修改:

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
}

4、Python 侧传递指针到仓颉侧:

为 Python 文件增加函数:

# File test.py

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

# `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'))

修改仓颉代码:

from std import 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
}
  • 由于仓颉侧调用函数不能将指针类型传递给 Python 库,所以该处仅支持在 Python 侧进行调用。

对其编译并执行:

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