使用新版本 (2024-07-19 16:10发布的)
1、仓颉-C 互操作
为了兼容已有的生态,仓颉支持调用 C 语言的函数,也支持 C 语言调用仓颉的函数
。
1.1 仓颉调用 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 函数
不支持命名参数
和参数默认值
。foreign 函数允许变长参数,使用...
表达,只能用于参数列表的最后。变长参数均需要满足 CType 约束,但不必是同一类型。 - 仓颉(CJNative 后端)虽然提供了栈扩容能力,但是
由于 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
1.2 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 并进行函数调用是危险行为,需要用户保证指针指向的是一个切实可用的函数地址,否则将发生运行时错误。
1.3 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