语言互操作
C 语言互操作
unsafe 上下文
因为 C 语言太容易造成不安全,所以仓颉规定所有和 C 语言互操作的功能都只能发生在 unsafe 上下文中。unsafe 上下文是用 unsafe 关键字引入的。
unsafe
关键字可以有以下几种用法:
- 修饰一个代码块。
unsafe
表达式的类型就是这个代码块的类型。 - 修饰一个函数。
所有的 unsafe
函数和 CFunc
类型函数必须在 unsafe
上下文中调用。
所有的 unsafe
函数,均不能作为一等公民使用,包括不能赋值给变量,不能作为实参或返回值使用,不能作为表达式使用,只能在 unsafe
上下文中被调用。
语法定义为:
unsafeExpression
: 'unsafe' '{' expressionOrDeclarations '}'
;
unsafeFunction
: 'unsafe' functionDefinition
;
调用 C 函数
foreign 关键字和 @C
仓颉编程语言要调用 C 函数,需要先在仓颉代码中声明这个函数且用 foreign
关键字修饰。foreign
函数只能存在于 top-level
作用域中,且仅包内可见,故不能使用其他任何函数修饰符。
@C
只支持修饰 foreign
函数、top-level
作用域中的非泛型函数和 struct
类型。在修饰 foreign
函数时,@C
可省略。如未特别说明,C 语言互操作章节中的 foreign
函数均视为 @C
修饰的 foreign
函数。
在 @C
修饰下, foreign
关键字只允许修饰 top-level
作用域中的非泛型函数。foreign
函数只是声明,没有函数体,其参数和返回类型均不可省略。用 foreign
修饰的函数使用原生 C ABI,且不会做名字修饰。
foreign func foo(): Unit
foreign var a: Int32 = 0 // compiler error
foreign func bar(): Unit { // compiler error
return
}
多个 foreign
函数声明,可以使用 foreign
块来声明。foreign
块是指在 foreign
关键字后使用一对花括号括起来的声明序列。foreign
块内仅可包含函数。在 foreign
块上添加注解等同于为 foreign
块中的每一个成员加上注解。
foreign {
func foo(): Unit
func bar(): Unit
}
foreign
函数需要能链接到同名的 C 函数,且参数和返回类型需要保持一致。只有满足于 CType
约束的类型可以被用于 foreign
函数的参数和返回类型。关于 CType
的定义,请参考下文的 CType 接口部分。
foreign
函数不支持命名参数和参数默认值。foreign
函数允许变长参数,使用 ...
表达,只能用于参数列表的最后。变长参数均需要满足 CType
约束,但不必是同一类型。
CFunc
仓颉中的 CFunc
类型函数是指可以被 C 语言代码调用的函数,共有以下三种形式:
@C
修饰的foreign
函数@C
修饰的仓颉函数- 类型为
CFunc
的lambda
表达式- 与普通的 lambda 表达式不同,
CFunc lambda
不能捕获变量。
- 与普通的 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
的参数和返回类型不允许依赖外部的泛型参数。
func call<T>(f: CFunc<(T) -> Unit>, x: T) where T <: CType { // error
unsafe { f(x) }
}
func f<T>(x: T) where T <: CType {
let g: CFunc<(T) -> T> = { x: T => x } // error
}
class A<T> where T <: CType {
let x: CFunc<(T) -> Unit> // error
}
inout 参数
在仓颉中调用 CFunc
时,其实参可以使用 inout
关键字修饰组成引用传值表达式,此时,该参数按引用传递。引用传值表达式的类型为 CPointer<T>
,其中 T
为 inout
表达式修饰的表达式的类型。
引用传值表达式具有以下约束:
- 仅可用于对
CFunc
的调用处; - 其修饰对象的类型必须满足
CType
约束,但不可以是CString
; - 其修饰对象必须是用
var
定义的变量; - 通过仓颉侧引用传值表达式传递到 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
}
}
调用约定
函数调用约定描述调用者和被调用者双方如何进行函数调用(如参数如何传递、栈由谁清理等),函数调用和被调用双方必须使用相同的调用约定才能正常运行。仓颉编程语言通过 @CallingConv
来表示各种调用约定,支持的调用约定如下:
-
CDECL,
CDECL
表示 clang 的 C 编译器在不同平台上默认使用的调用约定。 -
STDCALL,
STDCALL
表示 Win32 API 使用的调用约定。
通过 C 语言互操作机制调用的 C 函数,未指定调用约定时将采用默认的CDECL
调用约定。如下调用 C 标准库函数 clock
示例:
@CallingConv[CDECL] // Can be omitted in default.
foreign func clock(): Int32
main() {
println(clock())
}
@CallingConv
只能用于修饰 foreign
块、单个 foreign
函数和 top-level
作用域中的 CFunc
函数。当 @CallingConv
修饰 foreign
块时,会为 foreign
块中的每个函数分别加上相同的 @CallingConv
修饰。
类型映射
在仓颉编程语言中声明 foreign 函数时,参数以及返回值的类型必须和要调用的 C 函数的参数和返回类型相一致。仓颉编程语言类型与 C 语言类型不同,有些简单值类型,如 C 语言 int32_t
类型可以直接使用仓颉编程语言 Int32 与之对应,但是一些相对复杂类型如结构体则需要在仓颉侧声明对应的同样内存布局的类型。仓颉编程语言可以用于和 C 交互的类型都满足 CType
约束,它们可以分成基础类型和复杂类型。基础类型包括整型、浮点型、Bool
类型、CPointer
类型、Unit
类型。复杂类型包括 用 @C
修饰的 struct
、CFunc
类型等。
基础类型
仓颉编程语言和C
之间传递参数时,基础的值类型会进行复制,如int
、short
等。仓颉编程语言与 C 语言相匹配的基础类型映射关系表,如下:
Cangjie Type | C type | Size |
---|---|---|
Unit | void | 0 |
Bool | bool | 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
类型等由于其在不同平台上的不确定性,需要程序员自行指定对应仓颉编程语言类型。IntNative/UIntNative
在 C 互操作场景中,其与 C 语言中的ssize_t/size_t
是一致的。- 在 C 互操作场景中,与 C 语言类似,
Unit
类型仅可作为CFunc
中的返回类型和CPointer
的泛型参数。
仓颉编程语言中 foreign
函数的参数、返回值的类型需要与 C 函数参数、返回值的类型相对应。对于类型映射关系明确且平台无关的类型(参考基础类型映射关系表),可以直接使用标准对应的仓颉编程语言基础类型。比如在 C 语言中有一个add
函数声明如下:
int64_t add(int64_t X, int64_t Y) { return X+Y; }
在仓颉编程语言中调用add
函数,代码示例如下:
foreign func add(x: Int64, y: Int64): Int64
main() {
let x1: Int64 = 42
let y1: Int64 = 42
var ret1 = unsafe { add(x1, y1) }
...
}
指针
仓颉编程语言提供 CPointer<T>
类型对应C
语言的指针 T*
类型,其中 T
必须满足 CType
约束。
CPointer
类型必须满足:
- 大小和对齐与平台相关
- 对它做加减法算术运算、读写内存,是需要在 unsafe 上下文操作的
CPointer<T1>
可以在 unsafe 上下文中使用类型强制转换,变成CPointer<T2>
类型
CPointer
有一些成员方法,如下所示:
func isNull() : bool
// operator overloading
unsafe operator func + (offset: int64) : CPointer<T>
unsafe operator func - (offset: int64) : CPointer<T>
// read and write access
unsafe func read() : T
unsafe func write(value: T) : Unit
// read and write with offset
unsafe func read(idx: int64) : T
unsafe func write(idx: int64, value: T) : Unit
CPointer
可以使用类型名构造一个实例,它的值对应 C 语言的 NULL
。
func test() {
let p = CPointer<Int64>()
let r1 = p.isNull() // r1 == true
}
仓颉支持将一个 CFunc
变量的类型转换为一个 CPointer
类型,其中 CPointer
的泛型参数 T
可以是满足 CType
约束的任意类型,使用方式如下:
foreign func rand(): Int32
main() {
var ptr = CPointer<Int8>(rand)
0
}
字符串
C 语言字符串实际是以'\0'终止的一维字符数组。仓颉编程语言提供 CString
与 C 语言字符串相匹配。通过 CString
的构造函数或 LibC
的 mallocCString
创建 C 语言字符串,如需在仓颉端释放,则调用 LibC
的 free
方法。
声明 foreign 函数时,需要根据要调用的 C 语言函数的声明来确定函数名称、参数类型、返回值类型。C 语言标准库printf
函数的声明如下:
int printf(const char *format, ...)
参数const char *
类型对应仓颉的类型为CString
。返回类型 int
对应的仓颉的 Int32
类型。创建字符串并调用printf
函数示例如下:
package demo
foreign func printf(fmt: CString, ...): Int32
main() {
unsafe {
let str: CString = LibC.mallocCString("hello world!\n")
printf(str)
LibC.free(str)
}
}
Array
类型
仓颉的 Array
类型是不满足 CType
约束的,因此它不能用于 foreign
函数的参数和返回值。但是当 Array
内部的元素类型满足 CType
约束的时候,仓颉允许使用下面这两个函数获取和释放指向数组内部元素的指针。
unsafe func acquireArrayRawData<T>(arr: Array<T>): CPointerHandle<T> where T <: CType
unsafe func releaseArrayRawData<T>(h: CPointerHandle<T>): Unit where T <: CType
struct CPointerHandle<T> {
let pointer: CPointer<T>
let array: Array<T>
}
参考如下示例,假设我们要把一个 Array<UInt8>
写入到文件中,可以这样做:
foreign func fwrite(buf: CPointer<UInt8>, size: UIntNative, count: UIntNative, stream: CPointer<Unit>): UIntNative
func writeFile(buffer: Array<UInt8>, file: CPointer<Unit>) {
unsafe {
let h = acquireArrayRawData(buffer)
fwrite(h.pointer, 1, buffer.size, file)
releaseArrayRawData(h)
}
}
VArray
类型
仓颉使用 VArray
类型与 C 数组类型映射。当 VArray<T, $N>
中的元素类型 T
满足CType
约束时, VArray<T, $N>
类型也满足 CType
约束。
struct A {} // A is not a CType.
let arr1: VArray<A, $2> // arr1 is not a CType.
let arr2: VArray<Int64, $2> // arr2 is a CType.
VArray
允许作为 CFunc 签名的参数类型,但不允许为返回类型。
如果 CFunc 签名中参数类型被声明为 VArray<T, $N>
,对应的实参也只能是被 inout
修饰的 VArray<T, $N>
类型的表达式,但参数传递时,仍然以 CPointer<T>
传递。
如果 CFunc 签名中参数类型被声明为 CPointer<T>
,对应的实参可以是被 inout
修饰的 VArray<T, $N>
类型的表达式,参数传递时,仍然为 CPointer<T>
类型。
CPointer<VArray<T, $N>>
等同于 CPointer<T>
。
foreign func foo1(a: VArray<Int32, $3>): Unit
foreign func foo2(a: CPointer<Int32>): Unit
var a: VArray<Int32, $3> = [1, 2, 3]
unsafe {
foo1(inout a) // Ok.
foo2(inout a) // Ok.
}
结构体
foreign
函数的签名中包含结构体类型时,需要在仓颉侧定义同样内存布局的 struct
,使用 @C
修饰。
参考如下示例,一个 C 图形库(libskialike.so)中有一个计算两点之间距离的函数distance
,其在 C 语言头文件中相关结构体和函数声明如下:
struct Point2D {
float x;
float y;
};
float distance(struct Point2D start, struct Point2D end);
声明 foreign
函数时,需要根据要调用的 C 语言函数的声明来确定函数名称、参数类型、返回值类型。当创建 C 侧结构体时,需要确定结构体各个成员名称和类型。代码示例如下:
package demo
@C
struct Point2D {
var x: Float32
var y: Float32
}
foreign func distance(start: Point2D, end: Point2D): Float32
用 @C
修饰的 struct
必须满足以下限制:
- 成员变量的类型必须满足
CType
约束 - 不能实现或者扩展
interfaces
- 不能作为
enum
的关联值类型 - 不允许被闭包捕获
- 不能具有泛型参数
用 @C
修饰的 struct
自动满足 CType
约束。
@C struct
中的 VArray 类型成员变量保证与 C 中数组的内存布局一致。
例如,对于以下 C 的结构体类型:
struct S {
int a[2];
int b[0];
}
在仓颉中,可以声明为如下结构体与 C 代码对应:
@C
struct S {
var a: VArray<Int32, $2> = VArray<Int32, $2>(item: 0)
var b: VArray<Int32, $0> = VArray<Int32, $0>(item: 0)
}
注意:C 语言中允许结构体的最后一个字段为未指明长度的数组类型,该数组被称为柔性数组(flexible array),仓颉不支持包含柔性数组的结构体的映射。
函数
仓颉中的函数类型是不满足 CType
约束的,故提供了 CFunc
作为 C 语言中函数指针的映射。如下 C 语言代码中定义的函数指针类型,在仓颉中可以映射为 CFunc<() -> Unit>
。
// Function pointer in C.
typedef void (*FuncPtr) ();
CFunc
的具体细节参见前面 CFunc
一节。
CType 接口
CType
接口是一个语言内置的空接口,它是 CType
约束的具体实现,所有 C 互操作支持的类型都隐式地实现了该接口,因此所有 C 互操作支持的类型都可以作为 CType
类型的子类型使用。
@C
struct Data {}
@C
func foo() {}
main() {
var c: CType = Data() // ok
c = 0 // ok
c = true // ok
c = CString(CPointer<UInt8>()) // ok
c = CPointer<Int8>() // ok
c = foo // ok
}
CType
接口是仓颉中的一个 interface
类型,它本身不满足 CType
约束。同时,CType
接口不允许被继承、显式实现、扩展。
CType
接口不会突破其子类型的使用限制。
@C
struct A {} // implicit implement CType
class B <: CType {} // error
class C {} // error
extend C <: CType {} // error
class D<T> where T <: CType {}
main() {
var d0 = D<Int8>() // ok
var d1 = D<A>() // ok
}