仓颉-C 互操作
为了兼容已有的生态,仓颉支持调用 C 语言的函数,也支持 C 语言调用仓颉的函数。
仓颉调用 C 的函数
在仓颉中要调用 C 的函数,需要在仓颉语言中用 @C
和 foreign
关键字声明这个函数,但 @C
在修饰 foreign
声明的时候,可以省略。
举个例子,假设我们要调用 C 的 rand
和 printf
函数,它的函数签名是这样的:
// 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)
}
}
需要注意的是:
foreign
修饰函数声明,代表该函数为外部函数。被foreign
修饰的函数只能有函数声明,不能有函数实现。foreign
声明的函数,参数和返回类型必须符合 C 和仓颉数据类型之间的映射关系(详见下节:类型映射)。- 由于 C 侧函数很可能产生不安全操作,所以调用
foreign
修饰的函数需要被unsafe
块包裹,否则会发生编译错误。 @C
修饰的foreign
关键字只能用来修饰函数声明,不可用来修饰其他声明,否则会发生编译错误。@C
只支持修饰foreign
函数、top-level
作用域中的非泛型函数和struct
类型。foreign
函数不支持命名参数和参数默认值。CJVM
目前不支持foreign
函数使用变长参数(...
),开发者遇到此类需求时,可以使用CPointer<T>
来代替。
一些不合法的 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 语言代码调用的函数,共有以下三种形式:
@C
修饰的foreign
函数@C
修饰的仓颉函数- 类型为
CFunc
的lambda
表达式,与普通的 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>): Int32
与 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>
,其中 T
为 inout
修饰的表达式的类型。
引用传值表达式具有以下约束:
- 仅可用于对
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
属性,当 unsafe
的 lambda
逃逸后,可以不在 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(unsafe { rand() })
}
@CallingConv
只能用于修饰 foreign
块、单个 foreign
函数和 top-level
作用域中的 CFunc
函数。当 @CallingConv
修饰 foreign
块时,会为 foreign
块中的每个函数分别加上相同的 @CallingConv
修饰。
类型映射
基础类型
仓颉与 C 语言支持基本数据类型的映射,总体原则是:
- 仓颉的类型不包含指向托管内存的引用类型;
- 仓颉的类型和 C 的类型具有同样的内存布局。
比如说,一些基本的类型映射关系如下:
Cangjie Type | C Type | Size (byte) |
---|---|---|
Unit | void | 0 |
Bool | bool | 1 |
UInt8 | char | 1 |
Int8 | int8_t | 1 |
UInt8 | uint8_t | 1 |
Int16 | int16_t | 2 |
UInt16 | uint16_t | 2 |
Int32 | int32_t | 4 |
UInt32 | uint32_t | 4 |
Int64 | int64_t | 8 |
UInt64 | uint64_t | 8 |
IntNative | ssize_t | platform dependent |
UIntNative | size_t | platform dependent |
Float32 | float | 4 |
Float64 | double | 8 |
说明:
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
注意:
目前 CJVM 还不支持在 foreign 函数中对 struct 类型直接进行值传递,开发者可以使用 CPointer
进行封装,使用引用传递。
用 @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 可以参考《仓颉编程语言库 API》。其中读写和偏移计算为不安全行为,当不合法的指针调用这些函数时,可能发生未定义行为,这些 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
强制类型转换为指针通常是安全的,但是不应该对转换后的指针执行任何的read
,write
操作,可能会导致运行时错误。
数组
VArray
是一种数据类型,主要用于与 C 的数组类型映射,VArray
可以用作 CFunc
函数参数或者 @C struct
的成员变量。当 VArray<T, $N>
中的元素类型 T
满足 CType
约束时, VArray<T, $N>
类型也满足 CType
约束。
但目前 CJVM
暂不支持 VArray
,在以下场景使用时,需要使用 CPinter<T>
代替 VArray<T, $N>
。
作为函数参数类型:
当 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>(repeat: 0)
var b = VArray<Int32, $0>(repeat: 0)
}
注意:
C 语言中允许结构体的最后一个字段为未指明长度的数组类型,该数组被称为柔性数组(flexible array),仓颉不支持包含柔性数组的结构体的映射。
字符串
特别地,对于 C 语言中的字符串类型,仓颉中设计了一个 CString
类型来对应。为简化为 C 语言字符串的操作,CString
提供了以下成员函数:
init(p: CPointer<UInt8>)
通过 CPointer 构造一个 CStringfunc getChars()
获取字符串的地址,类型为CPointer<UInt8>
func size(): Int64
计算该字符串的长度func isEmpty(): Bool
判断该字符串的长度是否为 0,如果字符串的指针为空返回 truefunc isNotEmpty(): Bool
判断该字符串的长度是否不为 0,如果字符串的指针为空返回 falsefunc isNull(): Bool
判断该字符串的指针是否为 nullfunc 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
仓颉还提供了 sizeOf
和 alignOf
两个函数,用于获取上述 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 互操作支持的类型的父类型,便于在泛型约束中使用。
需要注意的是:
CType
接口是仓颉中的一个接口类型,它本身不满足CType
约束;CType
接口不允许被继承、扩展;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 => println("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]
。
关于仓颉编译器支持的所有编译选项,详见cjc 编译选项。
C 作为程序入口调用仓颉
仓颉提供以 C 代码作为程序入口调用仓颉的功能。
仓颉提供了 invoke_api.h
头文件,该文件位置位于 ${CANGJIE_HOME}/include/invoke_api.h
,其中包含了初始化仓颉运行时、查找和调用仓颉函数的函数声明,开发者需要先在 C 程序中引入该 invoke_api.h
头文件,然后可以使用以下 API。
该功能的大致使用流程如下:初始化仓颉运行时,查找仓颉函数,调用仓颉函数,销毁仓颉运行时。
注意:
使用时请严格遵守该流程,未初始化仓颉运行时,调换查找、调用顺序等操作可能会造成未定义行为。
以下代码是 C 的伪代码,仅模拟流程,不能直接运行,可运行的样例代码请参见 [示例 2](#示例 2)。
#include "invoke_api.h
main() {
CJErrorCode code;
CangjieRT cangjieRT;
char* args[] = {0};
// 初始化仓颉运行时
code = initCangjieRuntime(&cangjieRT, args);
CJFunction func;
// 查找仓颉函数
code = findCangjieFunction(&func, cangjieRT, "test", "foo");
long retLoc;
// 调用仓颉函数
code = invokeCJFunction(&retLoc, NULL, cangjieRT, func, 42);
// 销毁仓颉运行时
destroyCangjieRuntime(cangjieRT);
}
仓颉错误码
CJErrorCode
是用来定义 API 返回状态的类型,开发者可以通过 CJErrorCode
来判断初始化运行时、函数调用等操作是否成功。
注意:
0 和正数返回值代表合理状态,负数返回值表示出现了非预期的错误,开发者在使用时应当保证程序中不会出现负数返回值。
typedef int32_t CJErrorCode;
const CJErrorCode CJ_OK = 0;
const CJErrorCode CJ_FAILED_WITH_EXCEPTION = 1;
const CJErrorCode CJ_HANDLE_ALREADY_RELEASED = 2;
const CJErrorCode CJ_TASK_IS_RUNNING = 3;
const CJErrorCode CJ_ILLEGAL_VALUE = -1;
const CJErrorCode CJ_NO_MEM = -2;
const CJErrorCode CJ_INTERNAL_ERROR = -3;
初始化仓颉运行时
/*
This function should be called once before any other interaction with CJ.
Parameters:
rt - pointer to the location where the resulting CangjieRT will be placed, which is INVALID_RT on failure.
params - an array of string arguments to initialize CJ Runtime. Should be zero-terminated.
Returns:
CJ_OK on success; otherwise, returns a suitable error code on failure.
*/
CJErrorCode initCangjieRuntime(CangjieRT* rt, char* params[]);
由于仓颉代码是在托管环境下运行的,GC、异常处理等功能需要在托管运行时(managed runtime)中才能正常执行,因此开发者需要在执行仓颉代码前先初始化仓颉运行时,然后在仓颉托管环境中执行仓颉代码。
注意:
开发者应当在执行任何其他操作前,首先执行初始化仓颉运行时操作。
初始化仓颉运行时需要给定一个启动参数列表 params
,params
是一个字符串数组,其中需要包含:
- cbc path:
-Djet.cbc.path=
,该路径需要包含所有需要加载的cbc
文件的绝对路径,以冒号(:
)分隔; - cbc main file:
-Djet.cbc.main.file=
,该参数需要指定需要加载的包含 main 方法的文件; - 运行时参数:请参考运行时环境变量章节,可以用于指定堆大小、GC 参数等;
- 以 0 结尾。
关于 params
的样例请见 [示例 2](#示例 2) 。
如果成功初始化仓颉运行时,则会将初始化好的仓颉运行时指针赋值给 CangjieRT* rt
,自身返回 CJ_OK
。
注意:
- 不支持在同一时间初始化多个仓颉运行时。
- 目前在 C 进程中只支持初始化一次仓颉运行时,销毁仓颉运行时后再次初始化仓颉运行时会失败,因此开发者需要注意销毁仓颉运行时的时机。
- 启动参数必须以
0
结尾,否则会造成未定义行为。
销毁仓颉运行时
/*
Function that should be called when no more interaction with Cangjie code is needed. All resources consumed by runtime are freed after that. If custom signal handlers were saved during call of initCangjieRuntime, it will be restored.
Parameters:
rt - the Cangjie RT that will be destroyed.
Returns:
CJ_OK on success; otherwise, returns a suitable error code on failure.
*/
CJErrorCode destroyCangjieRuntime(CangjieRT rt);
开发者可以在确认后续没有执行仓颉代码的情况下,手动销毁仓颉虚拟机,该操作会释放所有仓颉运行时消耗的资源。
如果成功销毁,则返回 CJ_OK
。
注意:
目前在 C 进程中只支持初始化一次仓颉运行时,销毁仓颉运行时后再次初始化仓颉运行时会失败,因此开发者需要注意销毁仓颉运行时的时机。
查找仓颉函数
/*
Finds public Cangjie @C function with the given name in the specified package. Signature should not be specified here as @C functions can't be overloaded in Cangjie.
Parameters:
function - pointer to the location where the resulting CJFunction will be placed, which is INVALID_CJ_FUNCTION on failure.
rt - the Cangjie RT instance.
packageName - a package name.
funcName - a function name.
Returns:
CJ_OK on success; otherwise, returns a suitable error code on failure.
*/
CJErrorCode findCangjieFunction(CJFunction* functionLoc, CangjieRT rt, char* packageName, char* funcName);
在指定的包 char* packageName
中根据指定的函数名 char* funcName
查找仓颉函数,请注意通过该方式只能查找到 @C 修饰的仓颉函数
,详情参见CFunc章节。
如果成功查找到仓颉函数,则会将查找到的仓颉函数指针赋值给 CJFunction* function
,自身返回 CJ_OK
。
注意:
- @C 修饰的 foreign 函数仅在仓颉侧有声明,而定义在 C 侧,本质上属于 C 函数,开发者可以在 C 上下文中直接寻找并调用该函数而无需使用
findCangjieFunction
API;- @C 修饰的仓颉函数其声明和定义都在仓颉侧,属于符合 C 定义的仓颉函数,并且可以在仓颉托管运行时中运行,因此可以被查找到并调用。
同步调用仓颉函数
/*
Parameters:
retLoc - pointer to the location where to put result.
exceptionDescriptionLoc - pointer to the location where resulting exception description will be stored. If pointer is NULL the description will not be prepared.
rt - CangjieRT instance that was created with initCangjieRuntime function.
func - Cangjie function found by findCangjieFunction.
Also, this function takes varargs, so, all needed by Cangjie function arguments could be passed there.
If invocation of Cangjie function was successful, the status is set to OK, result is stored in the location pointed by retLoc, and exception_description is NULL.
However, if there was an exception: the status is set to FAILED_WITH_EXCEPTION and nothing is written into retLoc. If exceptionDescriptionLoc is not NULL the C string with description of exception is allocated: it contains name of exception, line number and stack trace. The address of this string is written into exceptionDescriptionLoc.
Otherwise, return a suitable error code on unexpected failure.
*/
CJErrorCode invokeCJFunction(void* retLoc, char** exceptionDescriptionLoc, CangjieRT rt, CJFunction func, ...);
在寻找到一个有效的 CJFunction
后,开发者可以使用 invokeCJFucntion
调用寻找到的仓颉函数,invokeCJFunction
会阻塞式地等待仓颉函数返回。这里使用可变长参数(varargs)来处理入参,开发者在使用时需要保证传参的数量和类型和仓颉函数保持一致。
- 如果仓颉函数成功执行完毕,则会将仓颉函数返回值赋值给
void* retLoc
,char** exceptionDescriptionLoc
赋值为NULL
,自身返回CJ_OK
; - 如果仓颉函数在执行的过程中抛出异常,则
void* retLoc
不会被赋值,且自身返回CJ_FAILED_WITH_EXCEPTION
。如果输入的char** exceptionDescriptionLoc
不为NULL
,则会将抛出的异常字符串地址赋值给char** exceptionDescriptionLoc
,其中包含异常名、调用栈和行号等信息,如果输入的char** exceptionDescriptionLoc
为NULL
则不予处理。
注意:
- 使用
invokeCJFunction
会阻塞式等待仓颉函数返回,开发者在使用时需要注意这里可能产生的性能问题;invokeCJFunction
使用可变长参数来处理传参,请注意这里出于综合考量没有对入参做类型检查,开发者需要严格保证传参的数量和类型可以和仓颉函数匹配上,否则可能会发生未定义行为;- 在传参时请注意目前 CJVM 还不支持的参数类型,包括不支持 VArray ,以及不支持对 struct 类型进行值传递。
- 在对
CangjieRT rt
,CJFunction func
等类型传参时请注意传值的正确性,如果传入非法指针可能会导致 C 代码的未定义行为。
异步调用仓颉函数
/*
This call is not blocking, it immediately returns when task is scheduled.
Parameters:
handle - pointer to the location where to put resulting handle.
rt - CangjieRT instance that was created with initCangjieRuntime function.
func - Cangjie function found by findCangjieFunction.
Also, this function takes varargs, so, all needed by Cangjie function arguments could be passed there.
Returns:
CJ_OK on success; otherwise, returns a suitable error code on failure.
*/
CJErrorCode runCJFunctionAsync(CJThreadHandle* handleLoc, CangjieRT rt, CJFunction func, ...);
在寻找到一个有效的 CJFunction
后,开发者可以使用 runCJFunctionAsync
来异步地调用寻找到的仓颉函数,runCJFunctionAsync
会向仓颉运行时提交一个并发运行的任务,然后不会等待,立即返回。这里使用可变长参数(varargs)来处理入参,开发者在使用时需要保证传参的数量与类型和被调用的仓颉函数保持一致。
如果 runCJFunctionAsync
向仓颉运行时提交任务成功,则会返回 CJ_OK
,并将执行仓颉函数的协程句柄赋值给 CJThreadHandle* handle
,开发者可以通过 CJThreadHandle* handle
来获取仓颉函数返回结果。
注意:
- 使用
runCJFunctionAsync
会向仓颉运行时提交一个任务,在协程上并发的运行,具体运行的情况由协程调度器实现决定;runCJFunctionAsync
并不保证任务的提交顺序和执行顺序一致;runCJFunctionAsync
使用可变长参数来处理传参,请注意这里出于综合考量没有对入参做类型检查,开发者需要严格保证传参的数量和类型可以和仓颉函数匹配上,否则可能会发生未定义行为;- 在传参时请注意目前 CJVM 还不支持的参数类型,包括不支持 VArray,以及不支持对 struct 类型进行值传递。
- 在对
CangjieRT rt
,CJFunction func
等类型传参时请注意传值的正确性,如果传入非法指针可能会导致 C 代码的未定义行为。
获取仓颉函数返回值
/*
Call of such function is blocking, if execution is still in progress, the caller will wait till it ends;
Parameters:
retLoc - an address of the location, where to store the result (or NULL in case of void functionctions);
exceptionDescriptionLoc - pointer to the location where resulting exception description will be stored. If pointer is NULL the description will not be prepared.
rt - CangjieRT instance that was created with initCangjieRuntime function.
handle - instance of handle that was created acquired with runCJFunctionAsync.
Returns:
On normal executions the value returned could be one of the following:
CJ_OK,
CJ_FAILED_WITH_EXCEPTION,
CJ_HANDLE_ALREADY_RELEASED
If invocation of Cangjie function was successful, the status is set to OK, result is stored in the location pointed by retLoc, and exception_description is NULL.
However, if there was an exception: the status is set to FAILED_WITH_EXCEPTION and nothing is written into retLoc. If exceptionDescriptionLoc is not NULL the C string with description of exception is allocated: it contains name of exception, line number and stack trace. The address of this string is written into exceptionDescriptionLoc.
If accessed handle was already released to the moment when getTaskResult is called, CJ_HANDLE_ALREADY_RELEASED is returned.
Otherwise, return a suitable error code on unexpected failure.
*/
CJErrorCode getTaskResult(void* retLoc, char** exceptionDescriptionLoc, CangjieRT rt, CJThreadHandle handle);
在获取到一个有效的仓颉线程句柄 CJThreadHandle handle
后,开发者可以使用 getTaskResult
来获取仓颉线程 CJThreadHandle handle
上执行的仓颉函数的返回结果,该操作是阻塞式的,如果调用 getTaskResult
时仓颉函数仍在执行,getTaskResult
会持续等待直接仓颉函数执行完毕。
- 如果仓颉函数成功执行完毕,则会将仓颉函数返回值赋值给
void* retLoc
,char** exceptionDescriptionLoc
赋值为NULL
,自身返回CJ_OK
; - 如果仓颉函数在执行的过程中抛出异常,则
void* retLoc
不会被赋值,且自身返回CJ_FAILED_WITH_EXCEPTION
。如果输入的char** exceptionDescriptionLoc
不为NULL
,则会将抛出的异常字符串地址赋值给char** exceptionDescriptionLoc
,其中包含异常名、调用栈和行号等信息,如果输入的char** exceptionDescriptionLoc
为NULL
则不予处理。 - 如果在调用
getTaskResult
时仓颉线程句柄已经被释放了,则不会对void* retLoc
和char** exceptionDescriptionLoc
赋值,自身返回CJ_HANDLE_ALREADY_RELEASED
。
注意:
使用
getTaskResult
会阻塞式等待仓颉函数返回,开发者在使用时需要注意这里可能产生的性能问题。
释放仓颉线程句柄
/*
Releases handle to reclaim memory that used to store the task result. If task is still running releaseCJThreadHandle will instantly return, without reclaiming any underlying memory.
Parameters:
handle - the instance of CJThreadHandle that should be released.
Returns:
CJ_OK on success; CJ_TASK_IS_RUNNING if the task is still running.
Otherwise, returns a suitable error code on failure.
*/
CJErrorCode releaseCJThreadHandle(CangjieRT rt, CJThreadHandle handle)
开发者在使用 getTaskResult
获取仓颉线程的返回值之后,可以使用 releaseCJThreadHandle
释放该仓颉线程所使用的资源。
如果仓颉线程 CJThreadHandle handle
所挂载的仓颉函数已经执行完毕,调用 getTaskResult
会立即释放该线程资源,自身返回 CJ_OK
,如果在调用 getTaskResult
时,CJThreadHandle handle
所挂载的仓颉函数仍在执行,则不会释放该线程资源,自身立即返回 CJ_TASK_IS_RUNNING
。
示例 1
这里演示一下如何使用 C 互操作以及 write/read
接口对一个结构体进行赋值和读取值。
C 代码如下:
// draw.c
#include<stdio.h>
#include<stdint.h>
typedef struct {
int64_t x;
int64_t y;
} Point;
typedef struct {
float x;
float y;
float z;
} Cube;
int32_t drawPicture(Point* point, Cube* cube) {
point->x = 1;
point->y = 2;
printf("Draw Point finished.\n");
printf("Before draw cube\n");
printf("%f\n", cube->x);
printf("%f\n", cube->y);
printf("%f\n", cube->z);
cube->x = 4.4;
cube->y = 5.5;
cube->z = 6.6;
printf("Draw Cube finished.\n");
}
仓颉代码如下:
// main.cj
@C
struct Point {
var x: Int64 = 0
var y: Int64 = 0
}
@C
struct Cube {
var x: Float32 = 0.0
var y: Float32 = 0.0
var z: Float32 = 0.0
init(x: Float32, y: Float32, z: Float32) {
this.x = x
this.y = y
this.z = z
}
}
foreign func drawPicture(point: CPointer<Point>, cube: CPointer<Cube>): Int32
main() {
let pPoint = unsafe { LibC.malloc<Point>() }
let pCube = unsafe { LibC.malloc<Cube>() }
var cube = Cube(1.1, 2.2, 3.3)
unsafe {
pCube.write(cube)
drawPicture(pPoint, pCube) // in which x, y will be changed
println(pPoint.read().x)
println(pPoint.read().y)
println(pCube.read().x)
println(pCube.read().y)
println(pCube.read().z)
LibC.free(pPoint)
LibC.free(pCube)
}
}
使用如下命令将 C 代码编译到动态库:
clang draw.c -fPIC -shared -o libdraw.so
编译仓颉代码的命令如下(以 CJVM 后端为例):
cjc -L . -l draw main.cj -o main.cbc
其中编译命令中 -L .
表示链接库时从当前目录查找(假设 libdraw.so
存在于当前目录),-l draw
表示链接的库的名字,编译成功后默认生成二进制文件 main
,执行二进制文件的命令如下:
LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH cj main.cbc
运行结果如下:
Draw Point finished.
Before draw cube
1.100000
2.200000
3.300000
Draw Cube finished.
1
2
4.400000
5.500000
6.600000
示例 2
这里演示一个使用 C 作为入口调用仓颉代码的例子。
仓颉代码如下:
package test1
@C
func foo(x: Int32, y: Int32): Int64 {
println("Hello from foo!")
Int64(x + y)
}
main() {}
C 代码如下:
#include "stdio.h"
#include "string.h"
#include "stdarg.h"
#include "stdlib.h"
#include "invoke_api.h"
int main(int argv, char* argc[]) {
if (argv <= 1) {
return -111;
}
char* path_pref = "-Djet.cbc.path=";
char* main_pref = "-Djet.cbc.main.file=";
char* main_suf = "/test1.cbc";
int arg_len = strlen(argc[1]);
char* path = malloc(strlen(path_pref) + arg_len + 1);
char* main = malloc(strlen(main_pref) + arg_len + strlen(main_suf) + 1);
if (!path || !main) {
return -222;
}
path[0] = '\0'; main[0] = '\0';
strcat(path, path_pref); strcat(path, argc[1]);
strcat(main, main_pref); strcat(main, argc[1]); strcat(main, main_suf);
char* args[] = {
path,
main,
0 // End with 0!!!
};
printf("Hello from C\n");
CangjieRT rt = 1;
CJErrorCode code = initCangjieRuntime(&rt, args);
if (code != CJ_OK) {
printf("%d\n", code);
return code;
}
printf("runtime loaded\n");
{
CJFunction func;
CJErrorCode code = findCangjieFunction(&func, rt, "test1", "foo");
if (code != CJ_OK) {
printf("code = %d\n", code);
return code;
}
long retLoc;
code = invokeCJFunction(&retLoc, NULL, rt, func, 10, 20);
if (code != CJ_OK) {
printf("code = %d\n", code);
return code;
}
printf("sync result = %ld\n", retLoc);
}
code = destroyCangjieRuntime(rt);
if (code != CJ_OK) {
printf("%d\n", code);
return code;
}
printf("Bye from C\n");
return 0;
}
编译命令如下:
cjc test1.cj -o test1.cbc
clang test1.c -o a.out -L${CANGJIE_HOME}/bin/jet/profile/develop/lib -lcjvm
执行命令如下:
LD_LIBRARY_PATH=${CANGJIE_HOME}/bin/jet/profile/develop/lib:$LD_LIBRARY_PATH ./a.out `pwd`
预期结果如下:
Hello from C
runtime loaded
Hello from foo!
sync result = 30
Bye from C