加载中...
仓颉文档阅读-语言规约XIV: 语言互操作

仓颉文档阅读-语言规约XIV: 语言互操作

周五 10月 17 2025
5088 字 · 23 分钟

此样式内容, 表示文档原文内容

语言互操作

C语言互操作

unsafe上下文

因为 C语言太容易造成不安全, 所以仓颉规定所有和 C语言互操作的功能都只能发生在unsafe上下文中

unsafe上下文是用unsafe关键字引入的

unsafe关键字可以有以下几种用法:

  • 修饰一个代码块

    unsafe表达式的类型就是这个代码块的类型

  • 修饰一个函数

所有的unsafe函数和CFunc类型函数必须在unsafe上下文中调用

所有的unsafe函数, 均不能作为一等公民使用, 包括不能赋值给变量, 不能作为实参或返回值使用, 不能作为表达式使用, 只能在unsafe上下文中被调用

语法定义为:

PLAINTEXT
unsafeExpression
    : 'unsafe' '{' expressionOrDeclarations '}'
    ;

unsafeFunction
    : 'unsafe' functionDefinition
    ;

因为, C语言不安全, 所以相关操作只能在unsafe上下文中使用

unsafe可以修饰函数 或 代码块

调用 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, 且不会做名字修饰

CANGJIE
foreign func foo(): Unit
foreign var a: Int32 = 0          // 编译错误, 只能声明非泛型函数
foreign func bar(): Unit {        // 编译错误, 不能由函数体
    return
}

多个foreign函数声明, 可以使用foreign块来声明

foreign块是指在foreign关键字后使用一对花括号括起来的声明序列

foreign块内仅可包含函数

foreign块上添加注解等同于为foreign块中的每一个成员加上注解

CANGJIE
foreign {
    func foo(): Unit
    func bar(): Unit
}

foreign函数需要能链接到同名的 C函数, 且参数和返回类型需要保持一致

只有满足于CType约束的类型, 可以被用于foreign函数的参数和返回类型

关于CType的定义, 请参考下文的CType接口部分

foreign函数不支持命名参数和参数默认值

foreign函数允许变长参数, 使用...表达, 只能用于参数列表的最后

变长参数均需要满足CType约束, 但不必是同一类型

foreign只能修饰顶层作用域的非泛型函数, 且只是声明, 不能拥有函数体

仓颉要调用C函数, 就必须要使用foreign声明同名函数

@C只支持修饰foreign函数、top-level作用域中的非泛型函数和struct类型

不过@C修饰foreign函数时, @C可省略

@C应该表示这是C语言可调用的函数, 而foreign应该用来修饰会被仓颉调用的C函数

CFunc

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

  • @C修饰的foreign函数

  • @C修饰的仓颉函数

  • 类型为CFunclambda表达式

    与普通的lambda表达式不同, CFunc lambda不能捕获变量

CANGJIE
// 情况 1: @C 修饰的 foreign 函数
foreign func free(ptr: CPointer<Int8>): Unit

// 情况 2: @C 修饰的仓颉函数
@C
func callableInC(ptr: CPointer<Int8>) {
    print("This function is defined in Cangjie.")
}

// 情况 3: 类型为 CFunc 的 lambda 表达式
let f1: CFunc<(CPointer<Int8>) -> Unit> = { ptr =>
    print("This function is defined with CFunc lambda.")
}

以上三种形式声明/定义的函数的类型均为CFunc<(CPointer<Int8>) -> Unit>

foreign函数是仓颉可以调用的C函数, 也是C代码可调用的C函数

@C修饰仓颉函数, 即 表示此为 C代码可调用的C函数

CFunc是一个泛型类型, 类型参数可以是仓颉函数类型, 可以赋值一个lambda表达式, 但不是闭包

CFunc对应 C语言的函数指针类型

这个类型为泛型类型, 其泛型参数表示该CFunc入参和返回值类型, 使用方式如下:

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

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

CFunc的参数和返回类型都需要满足CType约束

或者说, 能够被C语言调用的C函数, 类型都需要满足CType约束

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

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

CANGJIE
main() {
    var ptr = CPointer<Int8>()
    var f = CFunc<() -> Unit>(ptr)
    unsafe { f() }                                                // 运行时 core dump, 因为指针是 nullptr
}

CFunc的参数和返回类型不允许依赖外部的泛型参数

CANGJIE
func call<T>(f: CFunc<(T) -> Unit>, x: T) where T <: CType {      // 错误
    unsafe { f(x) }
}

func f<T>(x: T) where T <: CType {
    let g: CFunc<(T) -> T> = { x: T => x }                        // 错误
}

class A<T> where T <: CType {
    let x: CFunc<(T) -> Unit>                                     // 错误
}

任意的CPointer<T>类型变量都可以转换成一个CFunc, 即使T并不表示一个函数类型

但, 如果此变量不指向一个有效的函数, 那么转换之后调用时会出现问题

且, T要满足CType约束

inout参数

在仓颉中调用CFunc时, 其 实参可以使用inout关键字修饰 组成引用传值表达式, 此时, 该参数按引用传递

引用传值表达式的类型为CPointer<T>, 其中Tinout表达式修饰的表达式的类型

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

  • 仅可用于对CFunc的调用处

  • 其修饰对象的类型必须满足CType约束, 但不可以是CString

  • 其修饰对象必须是用var定义的变量

  • 通过仓颉侧引用传值表达式传递到 C 侧的指针, 仅保证在函数调用期间有效, 即此种场景下 C 侧不应该保存指针以留作后用

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

CANGJIE
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 是通过类 A 的实例成员变量间接派生的
    }
}

inout关键字, 可以在调用C函数传参时 修饰实参, 表示传引用传参

但, 实参不能来自class类型实例, 因为需要在调用过程中保证参数的有效, 但类是由运行时自动管理的

其次, 因为类实例与成员之间已经存在一定的引用关系, 可能不能简单地映射到C指针语义

调用约定

函数调用约定, 描述调用者和被调用者双方 如何进行函数调用(如参数如何传递、栈由谁清理等), 函数调用和被调用双方 必须使用相同的调用约定才能正常运行

仓颉编程语言通过@CallingConv来表示各种调用约定, 支持的调用约定如下:

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

  • STDCALL, STDCALL表示Win32 API使用的调用约定

通过C语言互操作机制调用的C函数, 未指定调用约定时将采用默认的CDECL调用约定

如下调用 C 标准库函数clock示例:

CANGJIE
@CallingConv[CDECL]               // 默认情况下可省略
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修饰的structCFunc类型等

仓颉的类型 与 C语言的类型不同, 但仓颉在声明foreign函数时, 要求 声明函数的参数和返回值必须要与调用的C函数保持一致

所以, 仓颉的类型可以与C语言类型进行映射

基础类型

仓颉编程语言和C之间传递参数时, 基础的值类型会进行复制, 如intshort

仓颉编程语言与 C 语言相匹配的基础类型映射关系表, 如下:

Cangjie TypeC typeSize
Unitvoid0
Boolbool1
Int8int8_t1
UInt8uint8_t1
Int16int16_t2
UInt16uint16_t2
Int32int32_t4
UInt32uint32_t4
Int64int64_t8
UInt64uint64_t8
IntNativessize_tplatform dependent
UIntNativesize_tkplatform dependent
Float32float4
Float64double8

注意:

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

仓颉编程语言中foreign函数的参数、返回值的类型需要与 C 函数参数、返回值的类型相对应

对于类型映射关系明确且平台无关的类型(参考基础类型映射关系表), 可以直接使用标准对应的仓颉编程语言基础类型

比如在 C 语言中有一个add函数声明如下:

C
int64_t add(int64_t X,  int64_t Y) { return X+Y; }

在仓颉编程语言中调用add函数, 代码示例如下:

CANGJIE
foreign func add(x: Int64, y: Int64): Int64

main() {
    let x1: Int64 = 42
    let y1: Int64 = 42
    var ret1 = unsafe { add(x1, y1) }
    ...
}

对于基础类型, 如果仓颉中的类型 与 C语言中的类型 映射关系明确且与平台无关

那么在仓颉中调用C函数时, 可以是指使用对应的仓颉类型

上部分中描述的基础类型, 仓颉和C之间都是可以直接相互映射的

指针

仓颉编程语言提供CPointer<T>类型对应C语言的指针T*类型, 其中T必须满足CType约束

CPointer类型必须满足:

  • 大小和对齐与平台相关

  • 对它做加减法算术运算、读写内存, 是需要在unsafe上下文操作的

  • CPointer<T1>可以在unsafe上下文中使用类型强制转换, 变成CPointer<T2>类型

CPointer有一些成员方法, 如下所示:

CANGJIE
func isNull() : bool

// 操作符重载
unsafe operator func + (offset: int64) : CPointer<T>
unsafe operator func - (offset: int64) : CPointer<T>

// 读和写访问
unsafe func read() : T
unsafe func write(value: T) : Unit

// 带偏移的读和写
unsafe func read(idx: int64) : T
unsafe func write(idx: int64, value: T) : Unit

CPointer可以使用类型名构造一个实例, 它的值对应 C 语言的NULL

CANGJIE
func test() {
    let p = CPointer<Int64>()
    let r1 = p.isNull()             // r1 == true
}

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

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

仓颉提供了CPointer<T>类型, 映射C语言的指针, T可以是满足CType约束的任意类型

并且, CPointer默认拥有一些成员方法

字符串

C语言字符串实际是以'\0'终止的一维字符数组

仓颉编程语言提供CString与 C语言字符串相匹配

通过CString的构造函数或LibCmallocCString创建 C 语言字符串, 如需在仓颉端释放, 则调用LibCfree方法

声明foreign函数时, 需要根据要调用的 C语言函数的声明来确定函数名称、参数类型、返回值类型

C语言标准库printf函数的声明如下:

C
int printf(const char *format, ...)

参数const char *类型对应仓颉的类型CString

返回类型int对应的仓颉的Int32类型

创建字符串并调用printf函数示例如下:

CANGJIE
package demo

foreign func printf(fmt: CString, ...): Int32

main() {
    unsafe {
        let str: CString = LibC.mallocCString("hello world!\n")
        printf(str)
        LibC.free(str)
    }
}

CString类型不支持直接使用仓颉的字符串字面量进行初始化, 只支持使用CPointer<UInt>的指针进行初始化

最方便的创建CString的方式, 还是LibC.mallocCString("")的方式, 不过要手动释放

Array类型

仓颉的Array类型是不满足CType约束的, 因此它不能用于foreign函数的参数和返回值

但是当Array内部的元素类型满足CType约束的时候, 仓颉允许使用下面这两个函数 获取和释放 指向数组内部元素的指针

CANGJIE
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>写入到文件中, 可以这样做:

CANGJIE
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)
    }
}

仓颉的Array类型, 当内部元素类型满足CType约束时, 可以使用仓颉提供的函数获取指向数组内部元素的指针, 只能整体获取

获取完成之后, 需要对指针进行释放操作, 释放只是解除了CPointerArray数据的关联, 而非释放原Array数据内存

CPointerHandle.pointer表示当前指针指向的元素地址, CPointerHandle.array表示当前指针指向的数组(可以使用[]读写), 可以以此修改原Array中的数据

VArray类型

仓颉使用VArray类型与 C数组类型映射

VArray<T, $N>中的元素类型T满足CType约束时, VArray<T, $N>类型也满足CType约束

CANGJIE
struct A {}                         // A 不是一个 CType.
let arr1: VArray<A, $2>             // arr1 不是一个 CType.
let arr2: VArray<Int64, $2>         // arr2 是一个 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>

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

Array不满足CType约束, 但是VArray可以满足CType约束

但, VArray只允许作为CFunc的参数类型, 而不允许作为返回类型

且, 参数传参只能使用inout修饰的VArray类型对象

但是, 有一点不理解, 为什么 CPointer<VArray<T, $N>>等同于CPointer<T>

结构体

foreign函数的签名中包含结构体类型时, 需要在仓颉侧定义同样内存布局的struct, 使用@C修饰

仓颉中, struct应该可以映射C结构体, 结构体需要用@C修饰, 且其内的成员类型应该需要满足CType约束

参考如下示例, 一个 C 图形库(libskialike.so)中有一个计算两点之间距离的函数distance, 其在 C 语言头文件中相关结构体和函数声明如下:

C
struct Point2D {
    float x;
    float y;
};
float distance(struct Point2D start, struct Point2D end);

声明foreign函数时, 需要根据要调用的 C 语言函数的声明来确定函数名称、参数类型、返回值类型

当创建 C侧结构体时, 需要确定结构体各个成员名称和类型

代码示例如下:

CANGJIE
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 的结构体类型:

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

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

CANGJIE
@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), 仓颉不支持包含柔性数组的结构体的映射

和预想的没错, @C struct成员需要保证类型满足CType约束

且, C语言结构体成员可以使用柔性数组, 但仓颉中不支持

如果需要调用的是C库中已存在的函数, 且C代码定义有自己的结构体, 需要保证@C struct的内存布局与C代码的结构体内存布局完全一致

函数

仓颉中的函数类型是不满足CType约束的, 故提供了CFunc作为 C 语言中函数指针的映射

如下 C 语言代码中定义的函数指针类型, 在仓颉中可以映射为CFunc<() -> Unit>

C
// Function pointer in C.
typedef void (*FuncPtr) ();

CFunc的具体细节参见前面 CFunc一节

CType接口

CType接口是一个语言内置的空接口, 它是CType约束的具体实现, 所有 C 互操作支持的类型都隐式地实现了该接口, 因此所有 C 互操作支持的类型都可以作为CType类型的子类型使用

CANGJIE
@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接口不会突破其子类型的使用限制

CANGJIE
@C
struct A {}                         // 隐式实现 CType

class B <: CType {}                 // error, CType 无法被显式实现

class C {}

extend C <: CType {}                // error, CType 无法被显式实现

class D<T> where T <: CType {}

main() {
    var d0 = D<Int8>()              // ok
    var d1 = D<A>()                 // ok
}

CType在仓颉中是一个空接口, 主要用于约束


Thanks for reading!

仓颉文档阅读-语言规约XIV: 语言互操作

周五 10月 17 2025
5088 字 · 23 分钟