仓颉编程语言开发指南(下)

文章目录


仓颉编程语言开发指南(上)

仓颉编程语言开发指南(上):http://t.csdnimg.cn/h7iqW


十五、宏

1.宏的简介

宏可以理解为一种特殊的函数。一般的函数在输入的值上进行计算,然后输出一个新的值,而宏的输入和输出都是程序本身。在输入一段程序(或程序片段,例如表达式),输出一段新的程序,这段输出的程序随后用于编译和执行。为了把宏的调用和函数调用区分开来,我们在调用宏时使用 @ 加上宏的名称。

让我们从一个简单的例子开始:假设我们想在调试过程中打印某个表达式的值,同时打印出表达式本身。

let x = 3
let y = 2
@dprint(x)        // 打印 "x = 3"
@dprint(x + y)    // 打印 "x + y = 5"

显然,dprint 不能被写为常规的函数,由于函数只能获得输入的值,不能获得输入的程序片段。但是,我们可以将 dprint 实现为一个宏。一个基本的实现如下:

macro package define

import std.ast.*

public macro dprint(input: Tokens): Tokens {
    let inputStr = input.toString()
    let result = quote(
        print($(inputStr) + " = ")
        println($(input)))
    return result
}

在解释每行代码之前,我们先测试这个宏可以达到预期的效果。首先,在当前目录下创建一个 macros 文件夹,并在 macros 文件夹中创建 dprint.cj 文件,将以上内容复制到 dprint.cj 文件中。另外在当前目录下创建 main.cj,包含以下测试代码:

import define.*

main() {
    let x = 3
    let y = 2
    @dprint(x)
    @dprint(x + y)
}

请注意,得到的目录结构如下:

// Directory layout.
src
|-- macros
|     `-- dprint.cj
`-- main.cj

在当前目录(src)下,运行编译命令:

cjc macros/*.cj --compile-macro
cjc main.cj -o main

然后运行 ./main,可以看到如下输出:

x = 3
x + y = 5

让我们依次查看代码的每个部分:

  • 第 1 行:macro package define
    宏必须声明在独立的包中(不能和其他 public 函数一起),含有宏的包使用 macro package 来声明。这里我们声明了一个名为 define 的宏包。
  • 第 2 行:import std.ast.*
    实现宏需要的数据类型,例如 Tokens 和后面会讲到的语法节点类型,位于仓颉标准库的 ast 包中,因此任何宏的实现都需要首先引入 ast 包。
  • 第 3 行:public macro dprint(input: Tokens): Tokens
    在这里我们声明一个名为 dprint 的宏。由于这个宏是一个非属性宏(之后我们会解释这个概念),它接受一个类型为 Tokens 的参数。该输入代表传给宏的程序片段。宏的返回值也是一个程序片段。
  • 第 4 行:let inputStr = input.toString()
    在宏的实现中,首先将输入的程序片段转化为字符串。在前面的测试案例中,inputStr 成为 “x” 或 “x + y”
  • 第 5-7 行:let result = quote(…)
    这里 quote 表达式是用于构造 Tokens 的一种表达式,它将括号内的程序片段转换为 Tokens。在 quote 的输入中,可以使用插值 $(...) 来将括号内的表达式转换为 Tokens,然后插入到 quote 构建的 Tokens 中。对于以上代码,$(inputStr) 插入 inputStr 字符串的值(包含字符串两端的引号),$(input) 插入 input,即输入的程序片段。因此,如果输入的表达式是 x + y,那么形成的Tokens为:
print("x + y" + " = ")
println(x + y)
  • 第 8 行:return result
    最后,我们将构造出来的代码返回,这两行代码将被编译,运行时将输出 x + y = 5。

回顾 dprint 宏的定义,我们看到 dprint 使用 Tokens 作为入参,并使用 quote 和插值构造了另一个 Tokens 作为返回值。为了使用仓颉宏,我们需要详细了解 Tokens、quote 和插值的概念,下面我们将分别介绍它们。

2.Tokens 相关类型和 quote 表达式

Token 类型

宏操作的基本类型是 Tokens,代表一个程序片段。Tokens 由若干个 Token 组成,每个 Token 可以理解为用户可操作的词法单元。一个 Token 可能是一个标识符(例如变量名等)、字面量(例如整数、浮点数、字符串)、关键字或运算符。每个 Token 包含它的类型、内容和位置信息。

Token 的类型取值为 enum TokenKind 中的元素。TokenKind 的可用值详见《仓颉编程语言库 API》文档。通过提供 TokenKind 和(对于标识符和字面量)Token 的字符串,可以直接构造任何 Token。具体的构造函数如下:

Token(k: TokenKind)
Token(k: TokenKind, v: String)

下面给出一些Token构造的例子:

import std.ast.*

let tk1 = Token(TokenKind.ADD)   // '+'运算符
let tk2 = Token(TokenKind.FUNC)   // func关键字
let tk3 = Token(TokenKind.IDENTIFIER, "x")   // x标识符
let tk4 = Token(TokenKind.INTEGER_LITERAL, "3")  // 整数字面量
let tk5 = Token(TokenKind.STRING_LITERAL, "xyz")  // 字符串字面量

Tokens 类型

一个 Tokens 代表由多个 Token 组成的序列。我们可以通过 Token 数组直接构造 Tokens。下面是 3 种基本的构造 Tokens 实例的方式:

Tokens()   // 构造空列表
Tokens(tks: Array<Token>)
Tokens(tks: ArrayList<Token>)

此外,Tokens 类型支持以下函数:

  • size:返回 Tokens 中包含 Token 的数量
  • get(index: Int64):获取指定下标的 Token 元素
  • []:获取指定下标的 Token 元素
  • +:拼接两个 Tokens,或者直接拼接 Tokens 和 Token
  • dump():打印包含的所有 Token,供调试使用
  • toString():打印 Tokens 对应的程序片段

在下面的案例中,我们使用构造函数直接构造 Token 和 Tokens,然后打印详细的调试信息:

import std.ast.*

let tks = Tokens(Array<Token>([
    Token(TokenKind.INTEGER_LITERAL, "1"),
    Token(TokenKind.ADD),
    Token(TokenKind.INTEGER_LITERAL, "2")
]))
main() {
    println(tks)
    tks.dump()
}

预期输出如下(具体的位置信息可能不同):

1 + 2
description: integer_literal, token_id: 140, token_literal_value: 1, fileID: 1, line: 4, column: 5
description: add, token_id: 12, token_literal_value: +, fileID: 1, line: 5, column: 5
description: integer_literal, token_id: 140, token_literal_value: 2, fileID: 1, line: 6, column: 5

在 dump 信息中,包含了每个 Token 的类型(description)和值(token_literal_value),最后打印每个 Token 的位置信息。

quote 表达式和插值

在大多数情况下,直接构造和拼接 Tokens 会比较繁琐。因此,仓颉语言提供了 quote 表达式来从代码模版来构造 Tokens。之所以说是代码模版,因为在 quote 中可以使用 $(…) 来插入上下文中的表达式。插入的表达式的类型需要支持被转换为 Tokens(具体来说,实现了 ToTokens 接口)。在标准库中,以下类型实现了 ToTokens 接口:

  • 所有的节点类型(节点将在语法节点中讨论)
  • Token 和 Tokens 类型
  • 所有基础数据类型:整数、浮点数、Bool、Rune和String
  • Array<T>ArrayList<T>,这里对 T 的类型有限制,并根据 T 的类型不同,输出不同的分隔符,详细请见《仓颉编程语言库 API》文档。

下面的例子展示 Array 和基础数据类型的插值:

import std.ast.*

let intList = Array<Int64>([1, 2, 3, 4, 5])
let float: Float64 = 1.0
let str: String = "Hello"
let tokens = quote(
    arr = $(intList)
    x = $(float)
    s = $(str)
)

main() {
    println(tokens)
}

输出结果是:

arr =[1, 2, 3, 4, 5]
x = 1.000000
s = "Hello"

更多插值的用法可以参考 使用 quote 插值语法节点。

特别地,当 quote 表达式包含某些特殊 Token 时,需要进行转义:

  • quote 表达式中不允许出现不匹配的小括号,但是通过 \ 转义的小括号,不计入小括号的匹配规则。
  • 当 $ 表示一个普通 Token,而非用于代码插值时,需要通过 \ 进行转义。
  • 除以上情况外,quote 表达式中出现 \ 会编译报错。

下面是一些 quote 表达式内包含这些特殊 Token 的例子:

import std.ast.*

let tks1 = quote((x))         // ok
let tks2 = quote(\()          // ok
let tks3 = quote( ( \) ) )    // ok
let tks4 = quote())           // error: unmatched delimiter: ')'
let tks5 = quote( ( \) )      // error: unclosed delimiter: '('

let tks6 = quote(\$(1))        // ok

let tks7 = quote(\x)           // error: unknown start of token: \

3.语法节点

在仓颉语言的编译过程中,首先通过词法分析将代码转换成 Tokens,然后对 Tokens 进行语法解析,得到一个语法树。每个语法树的节点可能是一个表达式、声明、类型、模式等。仓颉 ast 库提供了每种节点对应的类,它们之间具有适当的继承关系。其中,主要的抽象类如下:

  • Node:所有语法节点的父类
  • TypeNode:所有类型节点的父类
  • Expr:所有表达式节点的父类
  • Decl:所有声明节点的父类
  • Pattern:所有模式节点的父类

具体节点的类型众多,具体细节请参考 《仓颉编程语言库 API》文档。在下面的案例中,我们主要使用以下节点:

  • BinaryExpr:二元运算表达式
  • FuncDecl:函数的声明

节点的解析

通过 ast 库,基本上每种节点都可以从 Tokens 解析。有两种调用解析的方法。

使用解析表达式和声明的函数

以下函数用于从 Tokens 解析任意的表达式或任意的声明:

  • parseExpr(input: Tokens): Expr:将输入的 Tokens 解析为表达式
  • parseExprFragment(input: Tokens, startFrom!: Int64 = 0): (Expr, Int64):将输入 Tokens 的一个片段解析为表达式,片段从 startFrom 索引开始,解析可能只消耗从索引 startFrom 开始的片段的一部分,并返回第一个未被消耗的 Token 的索引(如果消耗了整个片段,返回值为 input.size)
  • parseDecl(input: Tokens, astKind!: String = “”):将输入的 Tokens 解析为声明,astKind 为额外的设置,具体请见《仓颉编程语言库 API》文档。
  • parseDeclFragment(input: Tokens, startFrom!: Int64 = 0): (Decl, Int64):将输入 Tokens 的一个片段解析为声明,startFrom 参数和返回索引的含义和 parseExpr 相同。

我们通过代码案例展示这些函数的使用:

let tks1 = quote(a + b)
let tks2 = quote(a + b, x + y)
let tks3 = quote(
    func f1(x: Int64) { return x + 1 }
)
let tks4 = quote(
    func f1(x: Int64) { return x + 1 }
    func f2(x: Int64) { return x + 2 }
)

let binExpr1 = parseExpr(tks1)
println("binExpr1 is BinaryExpr: ${binExpr1 is BinaryExpr}")
let (binExpr2, mid) = parseExprFragment(tks2)
let (binExpr3, end) = parseExprFragment(tks2, startFrom: mid + 1) // 跳过逗号
println("size = ${tks2.size}, mid = ${mid}, end = ${end}")
let funcDecl1 = parseDecl(tks3)
println("funcDecl1 is FuncDecl: ${funcDecl1 is FuncDecl}")
let (funcDecl2, mid2) = parseDeclFragment(tks4)
let (funcDecl3, end2) = parseDeclFragment(tks4, startFrom: mid2)
println("size = ${tks4.size}, mid = ${mid2}, end = ${end2}")

输出结果是:

binExpr1 is BinaryExpr: true
size = 7, mid = 3, end = 7
funcDecl1 is FuncDecl: true
size = 29, mid = 15, end = 29
使用构造函数进行解析

大多数节点类型都支持 init(input: Tokens) 构造函数,将输入的 Tokens 解析为相应类型的节点,例如:

import std.ast.*

let binExpr = BinaryExpr(quote(a + b))
let funcDecl = FuncDecl(quote(func f1(x: Int64) { return x + 1 }))

如果解析失败将抛出异常。这种解析方式适用于类型已知的代码片段,解析后不需要再手动转换成具体的子类型。

节点的组成部分

从 Tokens 解析出节点之后,我们可以查看节点的组成部分。作为例子,我们列出 BinaryExpr 和 FuncDecl 的组成部分,关于其他节点的更详细的解释请见《仓颉编程语言库 API》文档。

  • BinaryExpr 节点:

    • leftExpr: Expr:运算符左侧的表达式
    • op: Token:运算符
    • rightExpr: Expr:运算符右侧的表达式
  • FuncDecl 节点(部分):

    • identifier: Token:函数名
    • funcParams: ArrayList<FuncParam>:参数列表
    • declType: TypeNode:返回值类型
    • block: Block:函数体
  • FuncParam节点(部分):

    • identifier: Token:参数名
    • paramType: TypeNode:参数类型
  • Block节点(部分):

    • nodes: ArrayList<Node>:块中的表达式和声明

每个组成部分都是 public mut prop,因此可以被查看和更新。我们通过一些例子展示更新的结果。

BinaryExpr 案例
let binExpr = BinaryExpr(quote(x * y))
binExpr.leftExpr = BinaryExpr(quote(a + b))
println(binExpr.toTokens())

binExpr.op = Token(TokenKind.ADD)
println(binExpr.toTokens())

输出结果是:

(a + b) * y
a + b + y

首先,通过解析,获得 binExpr 为节点 x * y,图示如下:

    *
  /   \
 x     y

第二步,我们将左侧的节点(即 x)替换为 a + b,因此,获得的语法树如下:

      *
    /   \
   +     y
  / \
 a   b

当输出这个语法树的时候,我们必须在 a + b 周围添加括号,得到 (a + b) * y(如果输出a + b * y,含义为先做乘法,再做加法,与语法树的含义不同)。ast 库具备在输出语法树时自动添加括号的功能。

第三步,我们将语法树根部的运算符从 * 替换为 +,因此得到语法树如下:

      +
    /   \
   +     y
  / \
 a   b

这个语法树可以输出为 a + b + y,因为加法本身就是左结合的,不需要在左侧添加括号。

FuncDecl 案例
let funcDecl = FuncDecl(quote(func f1(x: Int64) { x + 1 }))
funcDecl.identifier = Token(TokenKind.IDENTIFIER, "foo")
println("Number of parameters: ${funcDecl.funcParams.size}")
funcDecl.funcParams[0].identifier = Token(TokenKind.IDENTIFIER, "a")
println("Number of nodes in body: ${funcDecl.block.nodes.size}")
let binExpr = (funcDecl.block.nodes[0] as BinaryExpr).getOrThrow()
binExpr.leftExpr = parseExpr(quote(a))
println(funcDecl.toTokens())

在这个案例中,我们首先通过解析构造出了一个 FuncDecl 节点,然后分别修改了该节点的函数名、参数名,以及函数体中表达式的一部分。输出结果是:

Number of parameters: 1
Number of nodes in body: 1
func foo(a: Int64) {
    a + 1
}

使用 quote 插值语法节点

任何 AST 节点都可以在 quote 语句中插值,部分 AST 节点的 ArrayList 列表也可以被插值(主要对应实际情况中会出现这类节点列表的情况)。插值直接通过 $(node) 表达即可,其中 node 是任意节点类型的实例。

下面,我们通过一些案例展示节点的插值。

var binExpr = BinaryExpr(quote(1 + 2))
let a = quote($(binExpr))
let b = quote($binExpr)
let c = quote($(binExpr.leftExpr))
let d = quote($binExpr.leftExpr)
println("a: ${a.toTokens()}")
println("b: ${b.toTokens()}")
println("c: ${c.toTokens()}")
println("d: ${d.toTokens()}")

输出结果是:

a: 1 + 2
b: 1 + 2
c: 1
d: 1 + 2.leftExpr

一般来说,插值运算符后面的表达式使用小括号限定作用域,例如 $(binExpr)。但是当后面只跟单个标识符的时候,小括号可省略,即可写为 $binExpr 。因此,在案例中 a 和 b 都在 quote 中插入了 binExpr节点,结果为 1 + 2。然而,如果插值运算符后面的表达式更复杂,不加小括号可能造成作用域出错。例如,表达式 binExpr.leftExpr 求值为 1 + 2 的左表达式,即 1,因此 c 正确赋值为 1。但 d 中的插值被解释为 ($binExpr).leftExpr,因此结果是 1 + 2.leftExpr。为了明确插值的作用域,我们推荐在插值运算符中使用小括号。

下面的案例展示节点列表(ArrayList)的插值。

var incrs = ArrayList<Node>()
for (i in 1..=5) {
    incrs.append(parseExpr(quote(x += $(i))))
}
var foo = quote(
    func foo(n: Int64) {
        let x = n
        $(incrs)
        x
    })
println(foo)

输出结果是:

func foo(n: Int64) {
    let x = n
    x += 1
    x += 2
    x += 3
    x += 4
    x += 5
    x
}

在这个案例中,我们创建了一个节点列表 incrs,包含表达式 x += 1,…,x += 5。对 incrs 的插值将节点依次列出,在每个节点后换行。这适用于插入需要依次执行的表达式和声明的情况。

下面的案例展示在某些情况下,需要在插值周围添加括号,以保证正确性。

var binExpr1 = BinaryExpr(quote(x + y))
var binExpr2 = BinaryExpr(quote($(binExpr1) * z))       // 错误:得到 x + y * z
println("binExpr2: ${binExpr2.toTokens()}")
println("binExpr2.leftExpr: ${binExpr2.leftExpr.toTokens()}")
println("binExpr2.rightExpr: ${binExpr2.rightExpr.toTokens()}")
var binExpr3 = BinaryExpr(quote(($(binExpr1)) * z))     // 正确:得到 (x + y) * z
println("binExpr3: ${binExpr3.toTokens()}")

输出结果是:

binExpr2: x + y * z
binExpr2.leftExpr: x
binExpr2.rightExpr: y * z
binExpr3: (x + y) * z

首先,我们构建了表达式 x + y,然后将该表达式插入到模版 $(binExpr1) * z 中。这里的意图是得到一个先计算 x + y,然后再乘 z 的表达式,但是,插值的结果是 x + y * z,先做 y * z,然后再加 x。这是因为插值不会自动添加括号以保证被插入的表达式的原子性(这和前一阶介绍的 leftExpr 的替换不同)。因此,需要在 $(binExpr1) 周围添加小括号,保证得到正确的结果。

4.宏的实现

本章节介绍仓颉宏的定义和使用,仓颉宏可以分为非属性宏和属性宏。同时本章节还会介绍宏出现嵌套时的行为。

非属性宏

非属性宏只接受被转换的代码,不接受其他参数(属性),其定义格式如下:

import std.ast.*

public macro MacroName(args: Tokens): Tokens {
    ... // Macro body
}

宏的调用格式如下:

@MacroName(...)

宏调用使用 () 括起来。括号里面可以是任意合法 Tokens,也可以是空。

当宏作用于声明时,一般可以省略括号。参考下面例子:

@MacroName func name() {}        // Before a FuncDecl
@MacroName struct name {}        // Before a StructDecl
@MacroName class name {}         // Before a ClassDecl
@MacroName var a = 1             // Before a VarDecl
@MacroName enum e {}             // Before a Enum
@MacroName interface i {}        // Before a InterfaceDecl
@MacroName extend e <: i {}      // Before a ExtendDecl
@MacroName mut prop i: Int64 {}  // Before a PropDecl
@MacroName @AnotherMacro(input)  // Before a macro call

对于括号里 Tokens 的合法性有以下特殊说明:

  • 输入的内容必须是由合法的 Token 组成的序列,类似 “#”、" ` “、”" 等符号单独使用都不是合法的仓颉 Token,不支持其作为输入值。
  • 输入的内容中,若存在不匹配的小括号则必须使用转义符号 “” 对其进行转义。
  • 输入的内容中,若希望 “@” 作为输入的 Token 则必须使用转义符号 “” 对其进行转义。

对于输入的特殊说明,可以参考下面例子:

// Illegal input Tokens
@MacroName(#)    // Not a whole Token
@MacroName(`)    // Not a whole Token
@MacroName(()    // ( and ) not match
@MacroName(\[)   // Escape for unsupported symbol

// Legal input Tokens
@MacroName(#"abc"#)
@MacroName(`class`)
@MacroName([)
@MacroName([])
@MacroName(\()
@MacroName(\@)

宏展开过程作用于仓颉语法树,宏展开后,编译器会继续进行后续的编译过程,因此,用户需要保证宏展开后的代码依然是合法的仓颉代码,否则可能引发编译问题。当宏用于声明时,如果省略括号,宏的输入必须是语法合法的声明,IDE 也会提供相应的语法检查和高亮。

下面是几个宏应用的典型示例。

  • 示例 1
    宏定义文件 macro_definition.cj
macro package macro_definition

import std.ast.*

public macro testDef(input: Tokens): Tokens {
    println("I'm in macro body")
    return input
}

宏调用文件 macro_call.cj

package macro_calling

import macro_definition.*

main(): Int64 {
    println("I'm in function body")
    let a: Int64 = @testDef(1 + 2)
    println("a = ${a}")
    return 0
}

上述代码的编译过程可以参考宏的编译和使用。

我们在用例中添加了打印信息,其中宏定义中的 I’m in macro body 将在编译 macro_call.cj 的期间输出,即对宏定义求值。同时,宏调用点被展开,如编译如下代码:

let a: Int64 = @testDef(1 + 2)

编译器将宏返回的 Tokens 更新到调用点的语法树上,得到如下代码:

let a: Int64 = 1 + 2

也就是说,可执行程序中的代码实际变为了:

main(): Int64 {
    println("I'm in function body")
    let a: Int64 = 1 + 2
    println("a = ${a}")
    return 0
}

a 经过计算得到的值为 3,在打印 a 的值时插值为 3。至此,上述程序的运行结果为:

I'm in function body
a = 3

下面看一个更有意义的用宏处理函数的例子,这个宏 ModifyFunc 宏的作用是给 MyFunc 增加 Composer 参数,并在counter++前后插入一段代码。

  • 示例 2
    宏定义文件 macro_definition.cj
// file macro_definition.cj
macro package macro_definition

import std.ast.*

public macro ModifyFunc(input: Tokens): Tokens {
    println("I'm in macro body")
    let funcDecl = FuncDecl(input)
    return quote(
        func $(funcDecl.identifier)(id: Int64) {
            println("start ${id}")
            $(funcDecl.block.nodes)
            println("end")
        })
}

宏调用文件 macro_call.cj

package macro_calling

import macro_definition.*

var counter = 0

@ModifyFunc
func MyFunc() {
    counter++
}

func exModifyFunc() {
    println("I'm in function body")
    MyFunc(123)
    println("MyFunc called: ${counter} times")
    return 0
}

main(): Int64 {
    exModifyFunc()
}

同样的,上述两段代码分别位于不同文件中,先编译宏定义文件 macro_definition.cj,再编译宏调用 macro_call.cj 生成可执行文件。

这个例子中,ModifyFunc 宏的输入是一个函数声明,因此可以省略括号:

@ModifyFunc
func MyFunc() {
    counter++
}

经过宏展开后,得到如下代码:

func MyFunc(id: Int64) {
    println("start ${id}")
    counter++
    println("end")
}

MyFunc 会在 main 中调用,它接受的实参也是在 main 中定义的,从而形成了一段合法的仓颉程序。运行时打印如下:

I'm in function body
start 123
end
MyFunc called: 1 times

属性宏

和非属性宏相比,属性宏的定义会增加一个 Tokens 类型的输入,这个增加的入参可以让开发者输入额外的信息。比如开发者可能希望在不同的调用场景下使用不同的宏展开策略,则可以通过这个属性入参进行标记位设置。同时,这个属性入参也可以传入任意 Tokens,这些 Tokens 可以与被宏修饰的代码进行组合拼接等。下面是一个简单的例子:

// Macro definition with attribute
public macro Foo(attrTokens: Tokens, inputTokens: Tokens): Tokens {
    return attrTokens + inputTokens  // Concatenate attrTokens and inputTokens.
}

如上面的宏定义,属性宏的入参数量为 2,入参类型为 Tokens,在宏定义内,可以对 attrTokens 和 inputTokens 进行一系列的组合、拼接等变换操作,最后返回新的 Tokens。

带属性的宏与不带属性的宏的调用类似,属性宏调用时新增的入参 attrTokens 通过 [] 传入,其调用形式为:

// attribute macro with parentheses
var a: Int64 = @Foo[1+](2+3)

// attribute macro without parentheses
@Foo[public]
struct Data {
    var count: Int64 = 100
}
  • 宏 Foo 调用,当参数是 2+3 时,与 [] 内的属性 1+ 进行拼接,经过宏展开后,得到 var a: Int64 = 1+2+3 。
  • 宏 Foo 调用,当参数是 struct Data 时,与 [] 内的属性 public 进行拼接,经过宏展开后,得到
public struct Data {
    var count: Int64 = 100
}

关于属性宏,需要注意以下几点:

  • 带属性的宏,与不带属性的宏相比,能修饰的 AST 是相同的,可以理解为带属性的宏对可传入参数做了增强。

  • 属性宏小括号内的参数合法性规则与非属性宏的参数合法性规则一致。

  • 属性宏中括号内的参数(属性)的合法性规则有如下特殊说明:

    • 输入的内容必须是由合法的 Token 组成的序列,类似 “#”、" ` “、”" 等符号单独使用都不是合法的仓颉 Token,不支持其作为输入值。
    • 输入的内容中,若存在不匹配的中括号则必须使用转义符号 “” 对其进行转义。
    • 输入的内容中,若希望 “@” 作为输入的 Token 则必须使用转义符号 “” 对其进行转义。
// Illegal attribute Tokens
@MacroName[#]()    // Not a whole Token
@MacroName[`]()    // Not a whole Token
@MacroName[@]()    // Not escape for @
@MacroName[[]()    // [ and ] not match
@MacroName[\(]()   // Escape for unsupported symbol

// Legal attribute Tokens
@MacroName[#"abc"#]()
@MacroName[`class`]()
@MacroName[(]()
@MacroName[()]()
@MacroName[\[]()
@MacroName[\@]()
  • 宏的定义和调用的类型要保持一致:如果宏定义有两个入参,即为属性宏定义,调用时必须加上 [],且内容可以为空;如果宏定义有一个入参,即为非属性宏定义,调用时不能使用 []。

嵌套宏

仓颉语言不支持宏定义的嵌套;有条件地支持在宏定义和宏调用中嵌套宏调用。

宏定义中嵌套宏调用

下面是一个宏定义中包含其他宏调用的例子。

宏包 pkg1 中定义 getIdent 宏:

macro package pkg1

import std.ast.*

public macro getIdent(attr:Tokens, input:Tokens):Tokens {
    return quote(
        let decl = (parseDecl(input) as VarDecl).getOrThrow()
        let name = decl.identifier.value
        let size = name.size - 1
        let $(attr) = Token(TokenKind.IDENTIFIER, name[0..size])
    )
}

宏包 pkg2 中定义 Prop 宏,其中嵌套了 getIdent 宏的调用:

macro package pkg2

import std.ast.*
import pkg1.*

public macro Prop(input:Tokens):Tokens {
    let v = parseDecl(input)
    @getIdent[ident](input)
    return quote(
        $(input)
        public prop $(ident): $(decl.declType) {
            get() {
                this.$(v.identifier)
            }
        }
    )
}

宏调用包 pkg3 中调用 Prop 宏:

package pkg3

import pkg2.*
class A {
    @Prop
    private let a_: Int64 = 1
}

main() {
    let b = A()
    println("${b.a}")
}

注意,按照宏定义必须比宏调用点先编译的约束,上述 3 个文件的编译顺序必须是:pkg1 -> pkg2 -> pkg3。pkg2 中的 Prop 宏定义:

public macro Prop(input:Tokens):Tokens {
    let v = parseDecl(input)
    @getIdent[ident](input)
    return quote(
        $(input)
        public prop $(ident): $(decl.declType) {
            get() {
                this.$(v.identifier)
            }
        }
    )
}

会先被展开成如下代码,再进行编译。

public macro Prop(input: Tokens): Tokens {
    let v = parseDecl(input)

    let decl = (parseDecl(input) as VarDecl).getOrThrow()
    let name = decl.identifier.value
    let size = name.size - 1
    let ident = Token(TokenKind.IDENTIFIER, name[0 .. size])

    return quote(
        $(input)
        public prop $(ident): $(decl.declType) {
            get() {
                this.$(v.identifier)
            }
        }
    )
}
宏调用中嵌套宏调用

嵌套宏的常见场景,是宏修饰的代码块中,出现了宏调用。一个具体的例子如下:

pkg1 包中定义 Foo 和 Bar 宏:

macro package pkg1

import std.ast.*

public macro Foo(input: Tokens): Tokens {
    return input
}

public macro Bar(input: Tokens): Tokens {
    return input
}

pkg2 包中定义 addToMul 宏:

macro package pkg2

import std.ast.*

public macro addToMul(inputTokens: Tokens): Tokens {
    var expr: BinaryExpr = match (parseExpr(inputTokens) as BinaryExpr) {
        case Some(v) => v
        case None => throw Exception()
    }
    var op0: Expr = expr.leftExpr
    var op1: Expr = expr.rightExpr
    return quote(($(op0)) * ($(op1)))
}

pkg3 包中使用上面定义的三个宏:

package pkg3

import pkg1.*
import pkg2.*
@Foo
struct Data {
    let a = 2
    let b = @addToMul(2+3)

    @Bar
    public func getA() {
        return a
    }

    public func getB() {
        return b
    }
}

main(): Int64 {
    let data = Data()
    var a = data.getA() // a = 2
    var b = data.getB() // b = 6
    println("a: ${a}, b: ${b}")
    return 0
}

如上代码所示,宏 Foo 修饰了 struct Data,而在 struct Data 内,出现了宏调用 addToMul 和 Bar。这种嵌套场景下,代码变换的规则是:将嵌套内层的宏(addToMul 和 Bar)展开后,再去展开外层的宏(Foo)。允许出现多层宏嵌套,代码变换的规则总是由内向外去依次展开宏。

嵌套宏可以出现在带括号和不带括号的宏调用中,二者可以组合,但用户需要保证没有歧义,且明确宏的展开顺序:

var a = @foo(@foo1(2 * 3)+@foo2(1 + 3))  // foo1, foo2 have to be defined.

@Foo1 // Foo2 expands first, then Foo1 expands.
@Foo2[attr: struct] // Attribute macro can be used in nested macro.
struct Data{
    @Foo3 @Foo4[123] var a = @bar1(@bar2(2 + 3) + 3)  // bar2, bar1, Foo4, Foo3 expands in order.
    public func getA() {
        return @foo(a + 2)
    }
}
嵌套宏之间的消息传递

这里指的是宏调用的嵌套。

内层宏可以调用库函数 assertParentContext 来保证内层宏调用一定嵌套在特定的外层宏调用中。如果内层宏调用这个函数时没有嵌套在给定的外层宏调用中,该函数将抛出一个错误。库函数 InsideParentContext 同样用于检查内层宏调用是否嵌套在特定的外层宏调用中,该函数返回一个布尔值。下面是一个简单的例子。

宏定义如下:

public macro Outer(input: Tokens): Tokens {
    return input
}

public macro Inner(input: Tokens): Tokens {
    assertParentContext("Outer")
    return input
}

宏调用如下:

@Outer var a = 0
@Inner var b = 0 // Error, The macro call 'Inner' should with the surround code contains a call 'Outer'.

如上代码所示,Inner 宏在定义时使用了 assertParentContext 函数用于检查其在调用阶段是否位于 Outer 宏中,在代码示例的宏调用场景下,由于 Outer 和 Inner 在调用时不存在这样的嵌套关系,因此编译器将报告一个错误。

内层宏也可以通过发送键/值对的方式与外层宏通信。当内层宏执行时,通过调用标准库函数 setItem 向外层宏发送信息;随后,当外层宏执行时,调用标准库函数 getChildMessages 接收每一个内层宏发送的信息(一组键/值对映射)。下面是一个简单的例子。

宏定义如下:

macro package define

import std.ast.*

public macro Outer(input: Tokens): Tokens {
    let messages = getChildMessages("Inner")

    let getTotalFunc = quote(public func getCnt() {
                       )
    for (m in messages) {
        let identName = m.getString("identifierName")
        // let value = m.getString("key")            // 接收多组消息
        getTotalFunc.append(Token(TokenKind.IDENTIFIER, identName))
        getTotalFunc.append(quote(+))
    }
    getTotalFunc.append(quote(0))
    getTotalFunc.append(quote(}))
    let funcDecl = parseDecl(getTotalFunc)

    let decl = (parseDecl(input) as ClassDecl).getOrThrow()
    decl.body.decls.append(funcDecl)
    return decl.toTokens()

}

public macro Inner(input: Tokens): Tokens {
    assertParentContext("Outer")
    let decl = parseDecl(input)
    setItem("identifierName", decl.identifier.value)
    // setItem("key", "value")                      // 可以通过不同的key值传递多组消息
    return input
}

宏调用如下:

import define.*

@Outer
class Demo {
    @Inner var state = 1
    @Inner var cnt = 42
}

main(): Int64 {
    let d = Demo()
    println("${d.getCnt()}")
    return 0
}

在上面的代码中,Outer 接收两个 Inner 宏发送来的变量名,自动为类添加如下内容:

public func getCnt() {
    state + cnt + 0
}

具体流程为:内层宏 Inner 通过 setItem 向外层宏发送信息;Outer 宏通过 getChildMessages 函数接收到 Inner 发送的一组信息对象(Outer 中可以调用多次 Inner);最后通过该信息对象的 getString 函数接收对应的值。

5.编译、报错与调试

宏的编译和使用

当前编译器约束宏的定义与宏的调用不允许在同一包里。宏包必须首先被编译,然后再编译宏调用的包。在宏调用的包中,不允许出现宏的定义。由于宏需在包中导出给另一个包使用,因此编译器约束宏定义必须使用 public 修饰。

下面介绍一个简单的例子。

源码目录结构如下:

// Directory layout.
src
`-- macros
      |-- m.cj

`-- demo.cj

宏定义放在 _macros_子目录下:

// macros/m.cj
// In this file, we define the macro Inner, Outer.
macro package define
import std.ast.*

public macro Inner(input: Tokens) {
    return input
}

public macro Outer(input: Tokens) {
    return input
}

宏调用代码如下:

// demo.cj
import define.*
@Outer
class Demo {
    @Inner var state = 1
    @Inner var cnt = 42
}

main() {
    println("test macro")
    0
}

以下为 Linux 平台的编译命令(具体编译选项会随着 cjc 更新而演进,以最新 cjc 的编译选项为准):

# 当前目录: src
# 先编译宏定义文件在当前目录产生默认的动态库文件(允许指定动态库的路径,但不能指定动态库的名字)
cjc macros/m.cj --compile-macro
# 编译使用宏的文件,宏替换完成,产生可执行文件
cjc demo.cj -o demo
# 运行可执行文件
./demo

在 Linux 平台上,将生成用于包管理的 macro_define.cjo 和实际的动态库文件。

若在 Windows 平台:

# 当前目录: src
# 先编译宏定义文件在当前目录产生默认的动态库文件(允许指定动态库的路径,但不能指定动态库的名字)
cjc macros/m.cj --compile-macro
# 编译使用宏的文件,宏替换完成,产生可执行文件
cjc demo.cj -o demo.exe

并行宏展开

可以在编译宏调用文件时添加 --parallel-macro-expansion 选项,启用并行宏展开的能力。编译器会自动分析宏调用之间的依赖关系,无依赖关系的宏调用可以并行执行,如上述例子中的两个 @Inner 就可以并行展开,如此可以缩短整体编译时间。

注意
如果宏函数依赖一些全局变量,使用并行宏展开会存在风险。

macro package define
import std.ast.*
import std.collection.*

var Counts = HashMap<String, Int64>()

public macro Inner(input: Tokens) {
    for (t in input) {
        if (t.value.size == 0) {
            continue
        }
        // 统计所有有效token value的出现次数
        if (!Counts.contains(t.value)) {
            Counts[t.value] = 0
        }
        Counts[t.value] = Counts[t.value] + 1
    }
    return input
}

public macro B(input: Tokens) {
    return input
}

参考上述代码,如果 @Inner 的宏调用出现在多处,并且启用了并行宏展开选项,则访问全局变量 Counts 就可能存在冲突,导致最后获取的结果不正确。

建议不要在宏函数中使用全局变量,如果必须使用,要么关闭并行宏展开选项,或者可以通过仓颉线程锁对全局变量进行保护。

diagReport 报错机制

仓颉 ast 包提供了自定义报错接口 diagReport。方便定义宏的用户,在解析传入 tokens 时,对错误 tokens 内容进行自定义报错。

自定义报错接口提供同原生编译器报错一样的输出格式,允许用户报 warning 和 error 两类错误提示信息。

diagReport 的函数原型如下:

public func diagReport(level: DiagReportLevel, tokens: Tokens, message: String, hint: String): Unit

其参数含义如下:

  • level: 报错信息等级
  • tokens: 报错信息中所引用源码内容对应的 tokens
  • message: 报错的主信息
  • hint: 辅助提示信息

参考如下使用示例。

宏定义文件:

// macro_definition.cj
macro package macro_definition

import std.ast.*

public macro testDef(input: Tokens): Tokens {
    for (i in 0..input.size) {
        if (input[i].kind == IDENTIFIER) {
            diagReport(DiagReportLevel.ERROR, input[i..(i + 1)],
                       "This expression is not allowed to contain identifier",
                       "Here is the illegal identifier")
        }
    }
    return input
}

宏调用文件:

// macro_call.cj
package macro_calling

import std.ast.*
import macro_definition.*

main(): Int64 {
    let a = @testDef(1)
    let b = @testDef(a)
    let c = @testDef(1 + a)
    return 0
}

编译宏调用文件过程中,会出现如下报错信息:

error: This expression is not allowed to contain identifier
 ==> call.cj:9:22:
  |
9 |     let b = @testDef(a)
  |                      ^ Here is the illegal identifier
  |

error: This expression is not allowed to contain identifier
  ==> call.cj:10:26:
   |
10 |     let c = @testDef(1 + a)
   |                          ^ Here is the illegal identifier
   |

2 errors generated, 2 errors printed.

使用 --debug-macro 输出宏展开结果

借助宏在编译期做代码生成时,如果发生错误,处理起来十分棘手,这是开发者经常遇到但一般很难定位的问题。这是因为,开发者写的源码,经过宏的变换后变成了不同的代码片段。编译器抛出的错误信息是基于宏最终生成的代码进行提示的,但这些代码在开发者的源码中没有体现。

为了解决这个问题,仓颉宏提供 debug 模式,在这个模式下,开发者可以从编译器为宏生成的 debug 文件中看到完整的宏展开后的代码,如下所示。

宏定义文件:

macro package define

import std.ast.*

public macro Outer(input: Tokens): Tokens {
    let messages = getChildMessages("Inner")

    let getTotalFunc = quote(public func getCnt() {
                       )
    for (m in messages) {
        let identName = m.getString("identifierName")
        getTotalFunc.append(Token(TokenKind.IDENTIFIER, identName))
        getTotalFunc.append(quote(+))
    }
    getTotalFunc.append(quote(0))
    getTotalFunc.append(quote(}))
    let funcDecl = parseDecl(getTotalFunc)

    let decl = (parseDecl(input) as ClassDecl).getOrThrow()
    decl.body.decls.append(funcDecl)
    return decl.toTokens()

}

public macro Inner(input: Tokens): Tokens {
    assertParentContext("Outer")
    let decl = parseDecl(input)
    setItem("identifierName", decl.identifier.value)
    return input
}

宏调用文件 demo.cj:

import define.*

@Outer
class Demo {
    @Inner var state = 1
    @Inner var cnt = 42
}

main(): Int64 {
    let d = Demo()
    println("${d.getCnt()}")
    return 0
}

在编译使用宏的文件时,在选项中,增加 --debug-macro,即使用仓颉宏的 debug 模式。

cjc --debug-macro demo.cj

在 debug 模式下,会生成临时文件 demo.cj.macrocall,对应宏展开的部分如下:

// demo.cj.macrocall
/* ===== Emitted by MacroCall @Outer in demo.cj:3:1 ===== */
/* 3.1 */class Demo {
/* 3.2 */    var state = 1
/* 3.3 */    var cnt = 42
/* 3.4 */    public func getCnt() {
/* 3.5 */        state + cnt + 0
/* 3.6 */    }
/* 3.7 */}
/* 3.8 */
/* ===== End of the Emit ===== */

如果宏展开后的代码有语义错误,则编译器的错误信息会溯源到宏展开后代码的具体行列号。仓颉宏的 debug 模式有以下注意事项:

  • 宏的 debug 模式会重排源码的行列号信息,不适用于某些特殊的换行场景。比如
// before expansion
@M{} - 2 // macro M return 2

// after expansion
// ===== Emmitted my Macro M at line 1 ===
2
// ===== End of the Emit =====
 - 2

这些因换行符导致语义改变的情形,不应使用 debug 模式。

  • 不支持宏调用在宏定义内的调试,会编译报错。
public macro M(input: Tokens) {
    let a = @M2(1+2) // M2 is in macro M, not suitable for debug mode.
    return input + quote($a)
}
  • 不支持带括号宏的调试。
// main.cj

main() {
    // For macro with parenthesis, newline introduced by debug will change the semantics
    // of the expression, so it is not suitable for debug mode.
    let t = @M(1+2)
    0
}

6.宏包定义和导入

仓颉宏的定义需要放在由 macro package 声明的包中,被 macro package 限定的包仅允许宏定义对外可见,其他声明包内可见。

说明
重导出的声明也允许对外可见,关于包管理和重导出的相关概念,请参见包的导入章节。

// file define.cj
macro package define         // 编译 define.cjo 携带 macro 属性
import std.ast.*

public func A() {}          // Error, 宏包不允许定义外部可见的非宏定义,此处需报错

public macro M(input: Tokens): Tokens { // macro M 外部可见
    return input
}

需要特殊说明的是,在 macro package 中允许其它 macro package 和非 macro package 符号被重导出,在非 macro package 中仅允许非 macro package 符号被重导出。

参考如下示例:

  • 在宏包 A 中定义宏 M1
macro package A
import std.ast.*

public macro M1(input: Tokens): Tokens {
    return input
}

编译命令如下:

cjc A.cj --compile-macro
  • 在非宏包 B 中定义一个 public 函数 f1。注意在非 macro package 中无法重导出 macro package 的符号
package B
// public import A.* // Error, it is not allowed to re-export a macro package in a package.

public func f1(input: Int64): Int64 {
    return input
}

编译命令如下,这里选择使用 --output-type 选项将 B 包编译成到动态库,关于 cjc 编译选项介绍可以参考cjc 编译选项章节。

cjc B.cj --output-type=dylib -o libB.so
  • 在宏包 C 中定义宏 M2,依赖了 A 包和 B 包的内容。可以看到 macro package 中可以重导出 macro package 和非 macro package 的符号
macro package C
public import A.* // correct: macro package is allowed to re-export in a macro package.
public import B.* // correct: non-macro package is also allowed to re-export in a macro package.
import std.ast.*

public macro M2(input: Tokens): Tokens {
    return @M1(input) + Token(TokenKind.NL) + quote(f1(1))
}

编译命令如下,注意这里需要显式链接 B 包动态库:

cjc C.cj --compile-macro -L. -lB
  • 在 main.cj 中使用 M2 宏
import C.*

main() {
    @M2(let a = 1)
}

编译命令如下:

cjc main.cj -o main -L. -lB

main.cj中 M2 宏展开后的结果如下:

import C.*

main() {
    let a = 1
    f1(1)
}

可以看到 main.cj 中出现了来自于 B 包的符号 f1。宏的编写者可以在 C 包中重导出 B 包里的符号,这样宏的使用者仅需导入宏包,就可以正确的编译宏展开后的代码。如果在 main.cj 中仅使用 import C.M2 导入宏符号,则会报 undeclared identifier ‘f1’ 的错误信息。

7.内置编译标记

仓颉语言提供了一些预定义的编译标记,可以通过这些编译标记控制仓颉编译器的编译行为。

源码位置

仓颉提供了几个内置编译标记,用于在编译时获取源代码的位置。

  • @sourcePackage() 展开后是一个 String 类型的字面量,内容为当前宏所在的源码的包名
  • @sourceFile() 展开后是一个 String 类型的字面量,内容为当前宏所在的源码的文件名
  • @sourceLine() 展开后是一个 Int64 类型的字面量,内容为当前宏所在的源码的代码行

这几个编译标记可以在任意表达式内部使用,只要能符合类型检查规则即可。示例如下:

func test1() {
    let s: String = @sourceFile()  // The value of `s` is the current source file name
}

func test2(n!: Int64 = @sourceLine()) { /* at line 5 */
    // The default value of `n` is the source file line number of the definition of `test2`
    println(n) // print 5
}

条件编译

条件编译使用 @When 标记,是一种在程序代码中根据特定条件选择性地编译不同代码段的技术。条件编译的作用主要体现在以下几个方面:

  • 平台适应:支持根据当前的编译环境选择性地编译代码,用于实现跨平台的兼容性。
  • 功能选择:支持根据不同的需求选择性地启用或禁用某些功能,用于实现功能的灵活配置。例如,选择性地编译包含或排除某些功能的代码。
  • 调试支持:支持调试模式下编译相关代码,用于提高程序的性能和安全性。例如,在调试模式下编译调试信息或记录日志相关的代码,而在发布版本中将其排除。
  • 性能优化:支持根据预定义的条件选择性地编译代码,用于提高程序的性能。

关于条件编译的具体内容,可以参考条件编译章节,这里不再额外展开。

@FastNative

为了提升与 C 语言互操作的性能,仓颉提供 @FastNative 标记用于优化对 C 函数的调用。值得注意的是 @FastNative 只能用于 foreign 声明的函数。

使用示例如下

@FastNative
foreign func strlen(str: CPointer<UInt8>): UIntNative

开发者在使用 @FastNative 修饰 foreign 函数时,应确保对应的 C 函数满足以下两点要求:

  1. 函数的整体执行时间不宜太长。例如:不允许函数内部存在很大的循环;不允许函数内部产生阻塞行为,如,调用 sleep、wait 等函数。
  2. 函数内部不能调用仓颉方法。

8.实用案例

快速幂的计算

我们通过一个简单的例子展示使用宏进行编译期求值,生成优化代码的应用。在计算幂 n ^ e 的时候,如果 e 是一个(比较大的)整数,可以通过重复取平方(而不是迭代相乘)的方式加速计算。这个算法可以直接使用 while 循环实现,例如:

func power(n: Int64, e: Int64) {
    var result = 1
    var vn = n
    var ve = e
    while (ve > 0) {
        if (ve % 2 == 1) {
            result *= vn
        }
        ve /= 2
        if (ve > 0) {
            vn *= vn
        }
    }
    result
}

然而,这个实现需要每次对 e 的值进行分析,在循环和条件判断中多次对 ve 进行判断和更新。此外,实现只支持 n 的类型为Int64的情况,如果要支持其他类型的 n,还要处理如何表达 result = 1 的问题。如果我们预先知道 e 的具体值,可以将这个代码写的更简单。例如,如果知道 e 的值为 10,我们可以展开整个循环如下:

func power_10(n: Int64) {
    var vn = n
    vn *= vn         // vn = n ^ 2
    var result = vn  // result = n ^ 2
    vn *= vn         // vn = n ^ 4
    vn *= vn         // vn = n ^ 8
    result *= vn     // result = n ^ 10
    result
}

当然,手动编写这些代码非常繁琐,我们希望在给定 e 的值之后,自动将这些代码生成出来。宏允许我们做到这一点。我们先看使用案例:

public func power_10(n: Int64) {
    @power[10](n)
}

这个宏展开的代码是(根据.macrocall文件):

public func power_10(n: Int64) {
    /* ===== Emitted by MacroCall @power in main.cj:20:5 ===== */
    /* 20.1 */var _power_vn = n
    /* 20.2 */_power_vn *= _power_vn
    /* 20.3 */var _power_result = _power_vn
    /* 20.4 */_power_vn *= _power_vn
    /* 20.5 */_power_vn *= _power_vn
    /* 20.6 */_power_result *= _power_vn
    /* 20.7 */_power_result
/* ===== End of the Emit ===== */
}

下面,我们看宏 @power 的实现。

macro package define

import std.ast.*
import std.convert.*

public macro power(attrib: Tokens, input: Tokens) {
    let attribExpr = parseExpr(attrib)
    if (let Some(litExpr) <- attribExpr as LitConstExpr) {
        let lit = litExpr.literal
        if (lit.kind != TokenKind.INTEGER_LITERAL) {
            diagReport(DiagReportLevel.ERROR, attrib,
                       "Attribute must be integer literal",
                       "Expected integer literal")
        }
        var n = Int64.parse(lit.value)
        var result = quote(var _power_vn = $(input)
        )
        var flag = false
        while (n > 0) {
            if (n % 2 == 1) {
                if (!flag) {
                    result += quote(var _power_result = _power_vn
                    )
                    flag = true
                } else {
                    result += quote(_power_result *= _power_vn
                    )
                }
            }
            n /= 2
            if (n > 0) {
                result += quote(_power_vn *= _power_vn
                )
            }
        }
        result += quote(_power_result)
        return result
    } else {
        diagReport(DiagReportLevel.ERROR, attrib,
                   "Attribute must be integer literal",
                   "Expected integer literal")
    }
    return input
}

这段代码的解释如下:

  • 首先,确认输入的属性 attrib 是一个整数字面量,否则通过 diagReport 报错。将这个字面量解析为整数 n。
  • 设 result 为当前积累的输出代码,首先添加 var _power_vn 的声明。这里为了避免变量名冲突,我们使用不易造成冲突的名字 _power_vn。
  • 下面进入 while 循环,布尔变量 flag 表示 var _power_result 是否已经被初始化。其余的代码结构和之前展示的 power 函数的实现类似,但区别是我们使用 while 循环和 if 判断在编译时决定生成的代码是什么,而不是在运行时做这些判断。最后生成由 _power_result *= _power_vn 和 _power_vn *= _power_vn 适当组合的代码。
  • 最后添加返回 _power_result 的代码。

将这段代码放到 macros/power.cj 文件中,并在 main.cj 添加如下测试:

public func power_10(n: Int64) {
    @power[10](n)
}

main() {
    let a = 3
    println(power_10(a))
}

输出结果为:

59049

Memoize 宏

Memoize(记忆化)是动态规划算法的常用手段。它将已经计算过的子问题的结果存储起来,当同一个子问题再次出现时,可以直接查询表来获取结果,从而避免重复的计算,提高算法的效率。

通常 Memoize 的使用需要开发者手动实现存储和提取的功能。通过宏,我们可以自动化这一过程。首先,让我们先看一下宏使用的效果:

@Memoize[true]
func fib(n: Int64): Int64 {
    if (n == 0 || n == 1) {
        return n
    }
    return fib(n - 1) + fib(n - 2)
}

main() {
    let start = DateTime.now()
    let f35 = fib(35)
    let end = DateTime.now()
    println("fib(35): ${f35}")
    println("execution time: ${(end - start).toMicroseconds()} us")
}

在以上代码中,fib 函数采用简单的递归方式实现。如果没有 @Memoize[true] 标注,这个函数的运行时间将随着 n 指数增长。例如,如果在前面的代码中去掉 @Memoize[true] 这一行,或者把 true 改为 false,则 main 函数的运行结果为:

fib(35): 9227465
execution time: 199500 us

恢复 @Memoize[true],运行结果为:

fib(35): 9227465
execution time: 78 us

相同的答案和大幅缩短的计算时间表明,@Memoize 的使用确实实现了记忆化。

现在让我们理解 @Memoize 的原理。首先,展示对以上 fib 函数进行宏展开的结果(来自 .macrocall 文件,但是为了提高可读性整理了格式)。

import std.collection.*

var _memoize_fib_map = HashMap<Int64, Int64>()

func fib(n: Int64): Int64 {
    if (_memoize_fib_map.contains(n)) {
        return _memoize_fib_map.get(n).getOrThrow()
    }

    let _memoize_eval_result = { =>
        if (n == 0 || n == 1) {
            return n
        }

        return fib(n - 1) + fib(n - 2)
    }()
    _memoize_fib_map.put(n, _memoize_eval_result)
    return _memoize_eval_result
}

上述代码的执行流程如下:

  • 首先,定义 _memoize_fib_map 为一个从 Int64 到 Int64 的哈希表,这里第一个 Int64 对应 fib 的唯一参数的类型,第二个 Int64 对应 fib 返回值的类型。
  • 其次,在函数体中,检查入参是否在 _memoize_fib_map 中,如果是则立即反馈哈希表中存储的值。否则,使用 fib 原来的函数体得到计算结果。这里使用了(不带参数的)匿名函数使 fib 的函数体不需要任何改变,并且能够处理任何从 fib 函数退出的方式(包括中间的 return,返回最后一个表达式等)。
  • 最后,把计算结果存储到 _memoize_fib_map 中,然后将计算结果返回。

有了这样一个“模版”之后,下面宏的实现就不难理解了。我们给出完整的代码如下。

public macro Memoize(attrib: Tokens, input: Tokens) {
    if (attrib.size != 1 || attrib[0].kind != TokenKind.BOOL_LITERAL) {
        diagReport(DiagReportLevel.ERROR, attrib,
                   "Attribute must be a boolean literal (true or false)",
                   "Expected boolean literal (true or false) here")
    }

    let memoized = (attrib[0].value == "true")
    if (!memoized) {
        return input
    }

    let fd = FuncDecl(input)
    if (fd.funcParams.size != 1) {
        diagReport(DiagReportLevel.ERROR, fd.lParen + fd.funcParams.toTokens() + fd.rParen,
                   "Input function to memoize should take exactly one argument",
                   "Expect only one argument here")
    }

    let memoMap = Token(TokenKind.IDENTIFIER, "_memoize_" + fd.identifier.value + "_map")
    let arg1 = fd.funcParams[0]

    return quote(
        var $(memoMap) = HashMap<$(arg1.paramType), $(fd.declType)>()

        func $(fd.identifier)($(arg1)): $(fd.declType) {
            if ($(memoMap).contains($(arg1.identifier))) {
                return $(memoMap).get($(arg1.identifier)).getOrThrow()
            }

            let _memoize_eval_result = { => $(fd.block.nodes) }()
            $(memoMap).put($(arg1.identifier), _memoize_eval_result)
            return _memoize_eval_result
        }
    )
}

首先,对属性和输入做合法性检查。属性必须是布尔字面量,如果为 false 则直接返回输入。否则,检查输入必须能够解析为函数声明(FuncDecl),并且必须包含正好一个参数。下面,产生哈希表的变量,取不容易造成冲突的变量名。最后,通过 quote 模版生成返回的代码,其中用到哈希表的变量名,以及唯一参数的名称、类型和输入函数的返回类型。

一个 dprint 宏的扩展

本节一开始使用了一个打印表达式的宏作为案例,但这个宏一次只能接受一个表达式。我们希望扩展这个宏,使其能够接受多个表达式,由逗号分开。我们展示如何使用 parseExprFragment 来实现这个功能。

宏的实现如下:

public macro dprint2(input: Tokens) {
    let exprs = ArrayList<Expr>()
    var index: Int64 = 0
    while (true) {
        let (expr, nextIndex) = parseExprFragment(input, startFrom: index)
        exprs.append(expr)
        if (nextIndex == input.size) {
            break
        }
        if (input[nextIndex].kind != TokenKind.COMMA) {
            diagReport(DiagReportLevel.ERROR, input[nextIndex..nextIndex+1],
                       "Input must be a comma-separated list of expressions",
                       "Expected comma")
        }
        index = nextIndex + 1  // 跳过逗号
    }
    let result = quote()
    for (expr in exprs) {
        result.append(quote(
            print($(expr.toTokens().toString()) + " = ")
            println($(expr))
        ))
    }
    return result
}

使用案例:

let x = 3
let y = 2
@dprint2(x, y, x + y)

输出结果为:

x = 3
y = 2
x + y = 5

在宏的实现中,使用 while 循环从索引 0 开始依次解析每个表达式。变量 index 保存当前解析的位置。每次调用 parseExprFragment 时,从当前位置开始,并返回解析后的位置(以及解析得到的表达式)。如果解析后的位置到达了输入的结尾,则退出循环。否则检查到达的位置是否是一个逗号,如果不是逗号,报错并退出,如果是逗号,跳过这个逗号并开始下一轮的解析。在得到表达式的列表后,依次输出每个表达式。

一个简单的 DSL

在这个案例中,我们展示如何使用宏实现一个简单的 DSL(Domain Specific Language,领域特定语言)。LINQ(Language Integrated Query,语言集成查询)是微软 .NET 框架的一个组成部分,它提供了一种统一的数据查询语法,允许开发者使用类似 SQL 的查询语句来操作各种数据源。在这里,我们仅展示一个最简单的 LINQ 语法的支持。

我们希望支持的语法为:

from <variable> in <list> where <condition> select <expression>

其中,variable 是一个标识符,list、condition 和 expression 都是表达式。因此,实现宏的策略是先后提取标识符和表达式,同时检查中间的关键字是正确的。最后,生成由提取部分组成的查询结果。

宏的实现如下:

public macro linq(input: Tokens) {
    let syntaxMsg = "Syntax is \"from <attrib> in <table> where <cond> select <expr>\""
    if (input.size == 0 || input[0].value != "from") {
        diagReport(DiagReportLevel.ERROR, input[0..1], syntaxMsg,
                   "Expected keyword \"from\" here.")
    }
    if (input.size <= 1 || input[1].kind != TokenKind.IDENTIFIER) {
        diagReport(DiagReportLevel.ERROR, input[1..2], syntaxMsg,
                   "Expected identifier here.")
    }
    let attribute = input[1]
    if (input.size <= 2 || input[2].value != "in") {
        diagReport(DiagReportLevel.ERROR, input[2..3], syntaxMsg,
                   "Expected keyword \"in\" here.")
    }
    var index: Int64 = 3
    let (table, nextIndex) = parseExprFragment(input, startFrom: index)
    if (nextIndex == input.size || input[nextIndex].value != "where") {
        diagReport(DiagReportLevel.ERROR, input[nextIndex..nextIndex+1], syntaxMsg,
                   "Expected keyword \"where\" here.")
    }
    index = nextIndex + 1  // 跳过where
    let (cond, nextIndex2) = parseExprFragment(input, startFrom: index)
    if (nextIndex2 == input.size || input[nextIndex2].value != "select") {
        diagReport(DiagReportLevel.ERROR, input[nextIndex2..nextIndex2+1], syntaxMsg,
                   "Expected keyword \"select\" here.")
    }
    index = nextIndex2 + 1  // 跳过select
    let (expr, nextIndex3) = parseExprFragment(input, startFrom: index)

    return quote(
        for ($(attribute) in $(table)) {
            if ($(cond)) {
                println($(expr))
            }
        }
    )
}

使用案例:

@linq(from x in 1..=10 where x % 2 == 1 select x * x)

这个例子从 1, 2, … 10 列表中筛选出奇数,然后返回所有奇数的平方。输出结果为:

1
9
25
49
81

可以看到,宏的实现的很大部分用于解析并校验输入的 tokens,这对宏的可用性至关重要。实际的 LINQ 语言(以及大多数 DSL)的语法更加复杂,需要一整套解析的机制,通过识别不同的关键字或连接符来决定下一步解析的内容。

十六、跨语言互操作

1.仓颉-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)
    }
}

需要注意的是:

  1. foreign 修饰函数声明,代表该函数为外部函数。被 foreign 修饰的函数只能有函数声明,不能有函数实现。
  2. foreign 声明的函数,参数和返回类型必须符合 C 和仓颉数据类型之间的映射关系(详见下节:类型映射)。
  3. 由于 C 侧函数很可能产生不安全操作,所以调用 foreign 修饰的函数需要被 unsafe 块包裹,否则会发生编译错误。
  4. @C 修饰的 foreign 关键字只能用来修饰函数声明,不可用来修饰其他声明,否则会发生编译错误。
  5. @C 只支持修饰 foreign 函数、top-level 作用域中的非泛型函数和 struct 类型。
  6. foreign 函数不支持命名参数和参数默认值。foreign 函数允许变长参数,使用 …表达,只能用于参数列表的最后。变长参数均需要满足 CType 约束,但不必是同一类型。
  7. 仓颉(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

CFunc

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

  1. @C 修饰的 foreign 函数
  2. @C 修饰的仓颉函数
  3. 类型为 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) -> Unit>。CFunc 对应 C 语言的函数指针类型。这个类型为泛型类型,其泛型参数表示该 CFunc 入参和返回值类型,使用方式如下:

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

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

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

仓颉语言支持将一个 CPointer 类型的变量类型转换为一个具体的 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 为 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 语言支持基本数据类型的映射,总体原则是:

  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 可以参考《仓颉编程语言库 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 约束的任意类型,使用方式如下:

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 可以用户作为函数参数和 @C struct 成员。当 VArray<T, $N> 中的元素类型 T 满足 CType 约束时, VArray<T, $N> 类型也满足 CType 约束。

作为函数参数类型:

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

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

仓颉还提供了 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 互操作支持的类型的父类型,便于在泛型约束中使用。

需要注意的是:

  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 => 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 库 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)
    }
}

编译仓颉代码的命令如下(以 CJNative 后端为例):

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

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

LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH ./main

2.仓颉-ArkTS 互操作

在 HarmonyOS 系统上,ArkTS 具备完整广泛的生态,为复用 ArkTS 生态,仓颉支持与 ArkTS 高效跨语言互通。

仓颉-ArkTS 互操作基于仓颉 CFFI 能力,通过调用 ArkTS 运行时接口,为用户提供库级别的 ArkTS 互操作能力。

使用场景:

  1. 在 ArkTS 应用开发仓颉模块:把用户仓颉代码封装成为 ArkTS 模块,能够被 ArkTS 代码加载和调用。
  2. 在仓颉应用里使用 ArkTS 存量库:在仓颉代码里创建新的 ArkTS 运行时,并加载和执行 ArkTS 的字节码。

互操作库的主要组成和功能:

  1. JSValue: 统一的 ArkTS 数据类型,在跨语言调用中做传参,对 ArkTS 类型做判断和做数据转换。
  2. JSContext: 一个 ArkTS 互操作上下文,用户创建 ArkTS 数据,辅助把 JSValue 转换为仓颉数据。
  3. JSCallInfo: 一次 ArkTS 函数调用的参数集合,包含所有的入参和 this 指针。
  4. JSRuntime: 一个由仓颉创建的 ArkTS 运行时。

在 ArkTS 应用里开发仓颉模块

开发仓颉互操作模块:

  1. 【仓颉侧】导入互操作库。
import ohos.ark_interop.*
  1. 【仓颉侧】定义要导出的函数,可被 ArkTS 调用的仓颉函数的类型是固定的:(JSContext, JSCallInfo)->JSValue。
func addNumber(context: JSContext, callInfo: JSCallInfo): JSValue {
    // 从 JSCallInfo 获取参数列表
    let arg0: JSValue = callInfo[0]
    let arg1: JSValue = callInfo[1]

    // 把 JSValue 转换为仓颉类型
    let a: Float64 = arg0.toNumber()
    let b: Float64 = arg1.toNumber()

    // 实际仓颉函数行为
    let value = a + b

    // 把结果转换为 JSValue
    let result: JSValue = context.number(value).toJSValue()

    // 返回 JSValue
    return result
}
  1. 【仓颉侧】注册要导出的函数。
// 类名没有影响
class Main {
    // 定义静态构造函数(也可用全局变量和静态变量的初始化表达式触发)
    static init() {
        // 注册键值对
        JSModule.registerModule {context, exports =>
            exports["addNumber"] = context.function(addNumber).toJSValue()
        }
    }
}
  1. 【ArkTS 侧】导入 ark_interop_loader,这是一个在 ohos-sdk 中提供的 napi 模块,作为仓颉运行时的启动器和仓颉模块的加载器。
import {requireCJLib} from "libark_interop_loader.so"
  1. 【ArkTS 侧】定义仓颉库导出的接口。
interface CangjieLib {
    // 定义的仓颉互操作函数,名称与仓颉侧注册名称一致。一般先定义 ArkTS 函数声明,在实现仓颉函数时根据声明来解析参数和返回。
    addNumber(a: number, b: number): number;
}
  1. 【ArkTS 侧】导入和调用仓颉库。
// 导入仓颉库,仓颉模块默认编译产物是 libentry.so,用户可以在 cjpm.toml 中修改配置。
const cjLib = requireCJLib("libentry.so") as CangjieLib;
// 调用仓颉接口
let result = cjLib.addNumber(1, 2);
console.log(`1 + 2 = ${result}`);

在仓颉应用里使用 ArkTS 模块

ArkTS 模块的编译产物主要有两种:

  1. C 代码(+ArkTS)编译成 so。
  2. 纯 ArkTS 代码编译成 abc。
加载 ArkTS so 模块

ArkTS so 模块根据部署方式的不同,分为以下几种:

  1. 随系统发布,在镜像的/system/lib64/module目录下。
  2. 随应用(hap)发布,在应用的/libs/arm64-v8a目录下,安装后在设备上的全局路径(通过hdc shell观察到的路径):/data/app/el1/bundle/public/${bundleName}/libs/arm64、沙箱路径(运行时可访问路径):/data/storage/el1/bundle/libs/arm64。
  3. 随动态库(hsp)发布。

这里主要介绍怎么加载随系统发布的 so 模块,这些 so 模块的详细介绍请参见 HarmonyOS 的官方文档。

接下来以相册管理模块作为示例,详细的介绍加载流程。

  1. 查看 ArkTS 文档,其导入模块的范本如下。
import photoAccessHelper from '@ohos.file.photoAccessHelper';
  1. 创建 ArkTS 运行时,准备互操作上下文。
import ohos.ark_interop.*

func tryLoadArkTSSo() {
    // 创建新的 ArkTS 运行时
    let runtime = JSRuntime()
    // 获取互操作上下文
    let context = runtime.mainContext
    ...
}
  1. 根据 ArkTS 文档里模块导入名称,推导仓颉的模块导入参数。
ArkTS 导入名仓颉导入参数说明
@ohos.file.photoAccessHelper(“file.photoAccessHelper”)以 @ohos 开头,那么参数只需要去掉 “@ohos.”。
@hms.core.push.pushService(“core.push.pushService”, prefix: “hms”)以非 @ohos 开头,那么参数去掉 “@xxx.”,并把 xxx 作为第二个参数。
  1. 导入 ArkTS 模块。
func tryLoadArkTSSo() {
    ...
    let module = context.requireSystemNativeModule("file.photoAccessHelper")
}

模块导入进来是一个 JSValue,接下来可以按照操作 ArkTS 数据的方法去操作模块。

在仓颉里操作 ArkTS 数据

从 ArkTS 传过来的参数,其原始类型是JSValue,这是一个匿名类型的数据,首先需要知晓其类型。

  • 通过JSValue.typeof()获取其类型枚举JSType。
  • 通过其他途径(包括但不限于阅读 ArkTS 源码、参考文档以及开发者口述)知晓其类型,然后通过类型校验接口来验证,比如判断是否是 number 类型JSValue.isNumber()。

当知道其类型之后,再把JSValue转换为对应的仓颉类型或 ArkTS 引用。

  • 转换为仓颉类型,比如一个 ArkTS string 转换为仓颉 String,JSValue.toString(JSContext)。
  • 转换为 ArkTS 引用,比如一个 ArkTS string 转换为 JSString,JSValue.asString(JSContext)。

通过仓颉数据来构造 ArkTS 数据,是通过 JSContext 的方法类构造 ArkTS 数据。

一个应用进程可以存在多个 ArkTS 运行时,而 ArkTS 运行时之间的数据是不通用的,任何 ArkTS 数据都归属于一个特定的运行时,因此创建 ArkTS 数据接口是从运行时的角度出发。

以number举例,创建一个number的方式是JSContext.number(Float64)。

ArkTS 主要数据类型对应到仓颉类型的映射如下:

ArkTS 类型仓颉类型安全引用typeof 类型
undefined-JSUndefinedJSType.UNDEFINED
null-JSNullJSType.NULL
booleanBoolJSBooleanJSType.BOOL
numberFloat64JSNumberJSType.NUMBER
stringStringJSStringJSType.STRING
object-JSObjectJSType.OBJECT
Array-JSArrayJSType.OBJECT
bigintBigIntJSBigIntJSType.BIGINT
function-JSFunctionJSType.FUNCTION
symbol-JSSymbolJSType.SYMBOL

安全引用的安全体现在两个方面:

  • 类型安全,特定类型的接口只能从安全引用里访问,总是需要先做显式的类型转换再访问。
  • 生命周期安全,对于由 ArkTS 来分配和回收的对象,安全引用能保障这些对象的生命周期。
操作 ArkTS 对象

从一个互操作函数的实现举例,该函数在 ArkTS 的声明是:addByObject(args: {a: number; b: number}): number。

func addByObject(context: JSContext, callInfo: JSCallInfo): JSValue {
    // 获取首个参数
    let arg0 = callInfo[0]
    // 校验参数0是否是对象,否则返回undefined
    if (!arg0.isObject()) {
        return context.undefined().toJSValue()
    }
    // 把参数0转换为JSObject
    let obj = arg0.asObject(context)
    // 从JSObject获取属性
    let argA = obj["a"]
    let argB = obj["b"]
    // 把JSValue转换为Float64
    let a = argA.toNumber()
    let b = argB.toNumber()

    let result = a + b
    return context.number(result).toJSValue()
}

除了可以从对象上读取属性外,还可以对属性赋值或创建新属性,操作方式为 JSObject[key] = value,其中 key 可以是仓颉 String 、JSString 或 JSSymbol,value 是 JSValue 。

说明
通过 JSObject[key] = value 定义属性时,该属性可写、可枚举、可配置。
更多参见JavaScript 标准内置对象。

对属性赋值在以下几种场景会失败,失败之后没有异常或日志:

  1. 目标对象是 sealed 对象,由 Object.seal() 接口创建的对象具有不可修改的特性,无法创建新的属性和修改原有属性。
  2. 目标属性的 writable 是 false ,由 Object.defineProperty(object, key, {writable: false, value: xxx}) 定义属性时,可以指定属性是否可写。

对于一个未知对象,可以枚举出该对象的可枚举属性:

func handleUnknownObject(context: JSContext, target: JSObject): Unit {
    // 枚举对象的可枚举属性
    let keys = target.keys()
    println("target keys: ${keys}")
}

创建一个新的 ArkTS 对象,可以通过 JSContext.object() 来创建。

对于 ArkTS 运行时,有一个特殊的对象,该对象是 ArkTS 全局对象,在任何 ArkTS 代码里都可以直接访问该对象下的属性,在仓颉侧可以通过 JSContext.global 来访问它。

调用 ArkTS 函数

拿到一个 ArkTS 函数后,可以在仓颉里直接调用,这里以一个互操作函数举例:addByCallback(a: number, b: number, callback: (result: number)=>void): void。

func addByCallback(context: JSContext, callInfo: JSCallInfo): JSValue {
    // 获取参数,并转换为Float64
    let a = callInfo[0].toNumber()
    let b = callInfo[1].toNumber()
    // 把第3个参数转换为JSFunction
    let callback = callInfo[2].asFunction(context)
    // 计算结果
    let result = a + b
    // 从仓颉Float64创建ArkTS number
    let retJSValue = context.number(result).toJSValue()
    // 调用回调函数
    callback.call(retJSValue)
}

这个用例里的函数是不带 this 指针的,针对需要 this 指针的方法调用,可以通过命名参数 thisArg 来指定。

func doSth(context: JSContext, callInfo: JSCallInfo): JSValue {
    let callback = callInfo[0].asFunction(context)
    let thisArg = callInfo[1]

    callback.call(thisArg: thisArg)
}

在 ArkTS 代码里,可以通过 对象.方法(…) 来进行调用,这时会隐式传递 this 指针。

class Someone {
    id: number = 0
    doSth(): void {
        console.log(`someone ${this.id} have done something`)
    }
}

let target = new Someone()

// 这里会隐式传递this指针,调用正常
target.doSth()

let doSth = target.doSth;
// 这里没有传递this指针,会出现异常`can't read property of undefined`
doSth.call()

在仓颉里,对应的写法如下:

func doSth(context: JSContext, callInfo: JSCallInfo): JSValue {
    let object = callInfo[0].asObject(context)
    // 会隐式传递this指针,调用正常
    object.callMethod("doSth")

    let doSth = object["doSth"].asFunction(context)
    // 未传递this指针,会出现异常`can't read property of undefined`
    doSth.call()
    // 显式传递this指针,调用正常
    doSth.call(thisArg: object.toJSValue())
}

在 ArkTS 里操作仓颉对象

这里用例展示的是把仓颉对象分享到 ArkTS 运行时,使用 ArkTS 运行时的内存管理机制来控制仓颉对象的生命周期,并通过相关的互操作接口来访问该对象。

// 定义共享类
class Data <: SharedObject {
    Data(
        // 定义2个属性
        let id: Int64,
        let name: String
    ) {}

    static init() {
        // 注册导出到ark的函数
        JSModule.registerFunc("createData", createData)
        JSModule.registerFunc("setDataId", setDataId)
        JSModule.registerFunc("getDataId", getDataId)
    }

    // 创建共享对象
    static func createData(context: JSContext, _: JSCallInfo): JSValue {
        // 创建仓颉对象
        let data = Data(1, "abc")
        // 创建js对仓颉对象的引用
        let jsExternal = context.external(data)
        // 返回js对仓颉对象的引用
        return jsExternal.toJSValue()
    }

    // 设置对象的id
    static func setDataId(context: JSContext, callInfo: JSCallInfo): JSValue {
        // 读取参数
        let arg0 = callInfo[0]
        let arg1 = callInfo[1]

        // 把参数0转换为js对仓颉对象的引用
        let jsExternal = arg0.asExternal(context)
        // 获取仓颉对象
        let data: Data = jsExternal.cast<Data>().getOrThrow()
        // 把参数1转换为Float64
        let value = arg1.toNumber()

        // 仓颉对象修改属性
        data.id = Int64(value)

        // 返回undefined
        let result = context.undefined().toJSValue()
        return result
    }

    // 获取对象的id
    static func getDataId(context: JSContext, callInfo: JSCallInfo): JSValue {
        let arg0 = callInfo[0]

        let jsExternal = arg0.asExternal(context)

        let data: Data = jsExternal.cast<Data>().getOrThrow()

        let result = context.number(Float64(data.id)).toJSValue()
        return result
    }
}
import {requireCJLib} from "libark_interop_loader.so"
// 定义导出符号
interface CustomLib {
    createData(): undefined
    setDataId(data: undefined, value: number): void
    getDataId(data: undefined): number
}

// 加载自定义库
const cjLib = requireCJLib("libentry.so") as CustomLib

// 创建共享对象
let data = cjLib.createData()
// 操作对象属性
cjLib.setDataId(data, 3)
let id = cjLib.getDataId(data)

console.log("id is " + id)

JSExternal 对象在 ArkTS 里的类型会被识别为 undefined,直接使用 undefined 来作为参数很容易被传递错误的参数会在运行时出错,如下示例:

...
// 创建共享对象
let data = cjLib.createData()
// 操作对象属性
cjLib.setDataId(undefined, 3) // 错误的参数,应该传递的是仓颉引用,但是编译器能通过编译
let id = cjLib.getDataId(data)
...
把仓颉对象的引用挂在 JSObject 上传递到 ArkTS

在实际开发接口时,可以把 JSExternal 对象绑定到一个 JSObject 对象上,把 JSExternal 的数据隐藏起来,以此来提高接口的安全性。

下面通过一个例子来展示:

// 定义共享类
class Data <: SharedObject {
    Data(
        // 定义2个属性
        var id: Int64,
        let name: String
    ) {}

    static init() {
        // 注册导出到ark的函数
        JSModule.registerFunc("createData", createData)
    }

    // 创建共享对象
    static func createData(context: JSContext, _: JSCallInfo): JSValue {
        let data = Data(1, "abc")
        let jsExternal = context.external(data)

        // 创建空JSObject
        let object = context.object()
        // 把js对仓颉对象的引用挂在JSObject的隐藏属性上
        object.attachCJObject(jsExternal)

        // 为js对象增加2个方法
        object["setId"] = context.function(setDataId).toJSValue()
        object["getId"] = context.function(getDataId).toJSValue()

        return object.toJSValue()
    }

    // 设置对象的id
    static func setDataId(context: JSContext, callInfo: JSCallInfo): JSValue {
        // 获取this指针
        let thisArg = callInfo.thisArg
        let arg0 = callInfo[0]

        // 把this指针转换为JSObject
        let thisObject = thisArg.asObject(context)
        // 从JSObject上获取隐藏属性
        let jsExternal = thisObject.getAttachInfo().getOrThrow()
        // 从js对仓颉对象的引用上获取仓颉对象
        let data = jsExternal.cast<Data>().getOrThrow()
        // 把参数0转换为Float64
        let value = arg0.toNumber()

        // 修改仓颉对象的属性
        data.id = Int64(value)

        let result = context.undefined()
        return result.toJSValue()
    }

    // 获取对象的id
    static func getDataId(context: JSContext, callInfo: JSCallInfo): JSValue {
        let thisArg = callInfo.thisArg
        let thisObject = thisArg.asObject(context)
        let jsExternal = thisObject.getAttachInfo().getOrThrow()
        let data = jsExternal.cast<Data>().getOrThrow()

        let result = context.number(Float64(data.id)).toJSValue()
        return result
    }
}
import {requireCJLib} from "libark_interop_loader.so"
// 定义导出符号
interface Data {
    setId(value: number): void
    getId(): number
}

interface CustomLib {
    createData(): Data
}

// 加载自定义库
const cjLib = requireCJLib("libentry.so") as CustomLib

// 创建共享对象
let data = cjLib.createData()
// 操作对象属性
data.setId(3)
let id = data.getId()

console.log("id is " + id)
为仓颉共享对象创建 JSClass

把所有的对象操作方法直接挂在对象上,一方面占用内存比较大,另一方面创建对象的开销比较大。对于追求性能的场景,可以定义一个 JSClass 来加速对象创建和减小内存占用。

// 定义共享类
class Data <: SharedObject {
    Data(
        // 定义2个属性
        var id: Int64,
        let name: String
    ) {}

    static init() {
        // 注册导出到ark的类
        JSModule.registerClass("Data") { context =>
            // 创建JSClass
            let clazz = context.clazz(jsConstructor)
            // 增加方法
            clazz.addMethod(context.string("setId"), context.function(setDataId))
            clazz.addMethod(context.string("getId"), context.function(getDataId))

            return clazz
        }
    }

    // js构造函数
    static func jsConstructor(context: JSContext, callInfo: JSCallInfo): JSValue {
        // 获取this指针
        let thisArg = callInfo.thisArg
        // 转换为JSObject
        let thisObject = thisArg.asObject(context)
        // 创建创建对象
        let data = Data(1, "abc")
        // 创建js对仓颉对象的引用
        let jsExternal = context.external(data)
        // 设置JSObject属性
        thisObject.attachCJObject(jsExternal)
        return thisObject.toJSValue()
    }

    // 设置对象的id
    static func setDataId(context: JSContext, callInfo: JSCallInfo): JSValue {
        // 获取this指针
        let thisArg = callInfo.thisArg
        // 把this指针转换为JSObject
        let thisObject = thisArg.asObject(context)
        // 从JSObject上获取隐藏属性
        let jsExternal = thisObject.getAttachInfo().getOrThrow()
        // 从js对仓颉对象的引用上获取仓颉对象
        let data = jsExternal.cast<Data>().getOrThrow()

        let arg0 = callInfo[0]
        // 把参数0转换为Float64
        let value = arg0.toNumber()

        // 修改仓颉对象的属性
        data.id = Int64(value)

        let result = context.undefined()
        return result.toJSValue()
    }

    // 获取对象的id
    static func getDataId(context: JSContext, callInfo: JSCallInfo): JSValue {
        let thisArg = callInfo.thisArg
        let thisObject = thisArg.asObject(context)
        let jsExternal = thisObject.getAttachInfo().getOrThrow()
        let data = jsExternal.cast<Data>().getOrThrow()

        let result = context.number(Float64(data.id)).toJSValue()
        return result
    }
}
import {requireCJLib} from "libark_interop_loader.so"
// 定义Data的接口
interface Data {
    setId(value: number): void
    getId(): number
}

interface CustomLib {
    // 定义Data的构造函数(JSClass)
    Data: {new (): Data}
}

// 加载自定义库
const cjLib  = requireCJLib("libentry.so") as CustomLib

// 创建共享对象
let data = new cjLib.Data()
// 操作对象属性
data.setId(3)
let id = data.getId()

console.log("id is " + id)

ArkTS 互操作和仓颉多线程

ArkTS 是单线程执行的虚拟机,在运行时上没有对并发做任何的容错;而仓颉在语法上支持内存共享的多线程。

如果在互操作的场景不加限制的使用多线程,可能会导致无法预期的错误,因此需要一些规范和指引来保证程序正常执行:

  1. ArkTS 代码以及大部分互操作接口只能在 ArkTS 线程上执行,否则会抛出仓颉异常。
  2. 在进入其他线程前,需要把所有依赖的 ArkTS 数据转换为仓颉数据。
  3. 在其他线程如果想要使用 ArkTS 接口,需要通过 context.postJSTask 切换到 ArkTS 线程来执行。

下面通过一个用例来展示具体做法,该用例是互操作函数,该函数的功能是对两个数字相加,并调用回调来返回相加数。

import {requireCJLib} from "libark_interop_loader.so"
// 定义导出的接口
interface CustomLib {
    addNumberAsync(a: number, b: number, callback: (result: number)=>void): void
}
// 导入仓颉库
const cjLib = requireCJLib("libentry.so") as CangjieLib;
// 调用仓颉函数
cjLib.addNumberAsync(1, 2, (result)=> {
    console.log("1 + 2 = " + result)
})
// 类名没有影响
class Main {
    // 定义静态构造函数
    static init() {
        // 注册键值对
        JSModule.registerFunc("addNumberAsync", addNumberAsync)
    }
}

func addNumberAsync(context: JSContext, callInfo: JSCallInfo): JSValue {
    // 从JSCallInfo获取参数列表
    let arg0: JSValue = callInfo[0]
    let arg1: JSValue = callInfo[1]
    let arg2: JSValue = callInfo[2]
    // 把JSValue转换为仓颉类型
    let a: Float64 = arg0.toNumber()
    let b: Float64 = arg1.toNumber()
    let callback = arg2.asFunction(context)
    // 新建仓颉线程
    spawn {
        // 实际仓颉函数行为
        let value = a + b
        // 发起异步回调
        context.postJSTask {
            // 创建result
            let result = context.number(value)
            // 调用js回调
            callback.call(result)
        }
    }

    // 返回 void
    return context.undefined()
}

在 ArkTS 存在着 Promise,这是对回调机制的一种封装,配合 async 、 await 的语法让回调机制变成同步调用的形式。对于上一个用例,使用 Promise 的形式来定义接口和访问:

// 接口定义
func addNumberAsync(context: JSContext, callInfo: JSCallInfo): JSValue {
    // 参数转换为仓颉类型
    let a = callInfo[0].toNumber()
    let b = callInfo[1].toNumber()
    // 创建PromiseCapability对象
    let promise = context.promiseCapability()
    // 创建新线程
    spawn {
        // 在新线程执行仓颉逻辑
        let result = a + b
        // 切换到ArkTS线程
        context.postJSTask {
            // 在ArkTS线程执行resolve
            promise.resolve(context.number(result).toJSValue())
        }
    }
    // 返回Promise
    promise.toJSValue()
}
// ArkTS 调用
import {requireCJLib} from "libark_interop_loader.so"
// 定义导出的接口
interface CustomLib {
    addNumberAsync(a: number, b: number): Promise<number>
}

async function main() {
    // 导入仓颉库
    const cjLib = requireCJLib("libentry.so") as CangjieLib;
    // 调用仓颉函数
    let result = await cjLib.addNumberAsync(1, 2)
    console.log("1 + 2 = " + result)
}

main()

仓颉与 ArkTS 互操作场景辅助库

基于仓颉与 ArkTS 互操作库,某些场景还向开发者提供辅助库,用于处理 ArkTS 与仓颉互操作业务。

仓颉应用中获取 JSAbilityContext

当开发者想要在仓颉应用中使用 ArkTS 模块时,某些场景,需要在 ArkTS 模块中的接口传入一个 context 入参。

例如,如下给出相机选择器 ArkTS 模块中的 pick 方法的声明,其第一个参数类型为 common.Context。

declare namespace cameraPicker {
  // ...
  function pick(context: Context, mediaTypes: Array<PickerMediaType>, pickerProfile: PickerProfile): Promise<PickerResult>
}

针对这类互操作场景,互操作 helper 库提供了一个 getJSContext 接口,可以将 AbilityContext 类型的 context 转换为互操作可调用的 JSValue。

public func getJSContext(runtime: JSRuntime, abilityContext: AbilityContext): JSValue

用如下示例说明:

import ohos.ark_interop.*
import ohos.ark_interop_helper.*

var runtime = Option<JSRuntime>.None
// We can get globalAbilityContext from the 'context' property of Ability
var globalAbilityContext: ?AbilityContext = None

// Get JSRuntime from the global variable.
func getRuntime() {
    return match (runtime) {
        case Some(v) => v
        case None =>
            let v = JSRuntime()
            runtime = v
            v
    }
}

// Get AbilityContext from the global varible which
// should initialized with the context property of Ability.
func getContext(): AbilityContext {
    match (globalAbilityContext) {
        case Some(context) =>
            context
        case _ =>
            AppLog.error("####getContext err ")
            throw Exception("get globalAbilityContext failed")
    }
}

// Get value of JSContext transformed from AbilityContext.
func getJSContextCase(): JSValue {
    let runtime = getRuntime()
    let abilityContext = getContext()
    getJSContext(runtime, abilityContext)
}

十七、编译和构建

1.cjc 使用

cjc是仓颉编程语言的编译命令,其提供了丰富的功能及对应的编译选项,本章将对基本使用方法进行介绍。

cjc-frontend (仓颉前端编译器)会随 cjc 一起通过 Cangjie SDK 提供,cjc-frontend 能够将仓颉源码编译至仓颉的中间表示 (LLVM IR)。 cjc-frontend 仅进行仓颉代码的前端编译,虽然 cjc-frontend 和 cjc 共享部分编译选项,但编译流程会在前端编译结束时中止。使用 cjc 时仓颉编译器会自动进行前端、后端的编译以及链接工作。cjc-frontend 仅作为前端编译器的实体体现提供,除编译器开发者外,仓颉代码的编译应优先使用 cjc 。

cjc 基本使用方法

想必你已经在学习仓颉的过程中尝试着使用 cjc 了,我们先来看一下 cjc 的基本使用方法,如果你想了解详细的编译选项内容,请查阅“cjc 编译选项”章节。

cjc 的使用方式如下:

cjc [option] file...

假如我们有一个名为 hello.cj 的仓颉文件:

main() {
    println("Hello, World!")
}

我们可以使用以下命令来编译此文件:

$ cjc hello.cj

此时工作目录下会新增可执行文件 main ,cjc 默认会将给定源代码文件编译成可执行文件,并将可执行文件命名为 main。

以上为不给任何编译选项时 cjc 的默认行为,我们可以通过使用编译选项来控制 cjc 的行为,例如让 cjc 进行整包编译,又或者是指定输出文件的名字。

2.cjpm 介绍

CJPM(Cangjie Package Manager) 是仓颉语言的官方包管理工具,用来管理、维护仓颉项目的模块系统,并且提供更简易统一的编译入口,支持自定义编译命令。通过包管理器自动依赖管理实现对引入的多版本三方依赖软件进行分析合并,无需开发者担心多版本依赖冲突问题,大大减轻开发者负担;同时提供基于仓颉语言原生的自定义构建机制,允许开发者在构建的不同阶段增加预处理和后处理流程,实现构建流程可灵活定制,能够满足开发者不同业务场景下的编译构建诉求。

cjpm 基本使用方法

通过 cjpm -h 即可查看主界面,由几个板块组成,从上到下分别是: 当前命令说明、使用示例(Usage)、支持的可用命令(Available subcommands)、支持的配置项(Available options)、更多提示内容。

Cangjie Package Manager

Usage:
  cjpm [subcommand] [option]

Available subcommands:
  init             Init a new cangjie module
  check            Check the dependencies
  update           Update cjpm.lock
  tree             Display the package dependencies in the source code
  build            Compile the current module
  run              Compile and run an executable product
  test             Unittest a local package or module
  clean            Clean up the target directory
  install          Install a cangjie binary
  uninstall        Uninstall a cangjie binary

Available options:
  -h, --help       help for cjpm
  -v, --version    version for cjpm

Use "cjpm [subcommand] --help" for more information about a command.

cjpm init 用来初始化一个新的仓颉模块或者工作空间。初始化模块时会默认在当前文件夹创建 cjpm.toml 文件,并且新建 src 源码文件夹,在 src 下生成默认的 main.cj 文件。自定义参数初始化功能支持可以通过cjpm init -h 查看。

例如:

输入: cjpm init
输出: cjpm init success

cjpm build 用来构建当前仓颉项目,执行该命令前会先检查依赖项,检查通过后调用 cjc 进行构建。支持全量编译、增量编译、交叉编译、并行编译等,更多编译功能支持可以通过cjpm build -h 查看。通过cjpm build -V 命令可以打印所有的编译过程命令。

例如:

输入: cjpm build -V
输出:
compile package module1.package1: cjc --import-path target -p "src/package1" --output-type=staticlib -o target/release/module1/libmodule1.package1.a
compile package module1: cjc --import-path target -L target/release/module1 -lmodule1.package1 -p "src" --output-type=exe --output-dir target/release/bin -o main

cjpm build success

cjpm.toml 配置文件说明

配置文件 cjpm.toml 用来配置一些基础信息、依赖项、编译选项等内容,cjpm 主要通过这个文件进行解析执行。

配置文件代码如下所示:

[package]
  cjc-version = "0.49.1" # 所需 `cjc` 的最低版本要求,必需
  name = "demo" # 模块名及模块 root 包名,必需
  description = "nothing here" # 描述信息,非必需
  version = "1.0.0" # 模块版本信息,必需
  compile-option = "" # 额外编译命令选项,非必需
  link-option = "" # 链接器透传选项,可透传安全编译命令,非必需
  output-type = "executable" # 编译输出产物类型,必需
  src-dir = "" # 指定源码存放路径,非必需
  target-dir = "" # 指定产物存放路径,非必需
  package-configuration = {} # 单包配置选项,非必需

[workspace] # 工作空间管理字段,与 package 字段不能同时存在
  members = []
  build-members = []
  test-members = []
  compile-option = ""
  link-option = ""
  target-dir = ""

[dependencies] # 源码依赖配置项
  coo = { git = "xxx",branch = "dev" , version = "1.0.0"} # 导入 `git` 依赖,`version`字段可缺省
  doo = { path = "./pro1" ,version = "1.0.0"} # 导入源码依赖,`version`字段可缺省

[test-dependencies] # 测试阶段的依赖配置项

[ffi.c] # 导入 `c` 库依赖
  clib1.path = "xxx"

[profile] # 命令剖面配置项
  build = {}
  test = {}
  customized-option = {}

[target.x86_64-unknown-linux-gnu] # 后端和平台隔离配置项
  compile-option = "value1" # 额外编译命令选项,适用于特定 target 的编译流程和指定该 target 作为交叉编译目标平台的编译流程,非必需
  link-option = "value2" # 链接器透传选项,适用于特定 target 的编译流程和指定该 target 作为交叉编译目标平台的编译流程,非必需

[target.x86_64-w64-mingw32.dependencies] # 适用于对应 target 的源码依赖配置项,非必需

[target.x86_64-w64-mingw32.test-dependencies] # 适用于对应 target 的测试阶段依赖配置项,非必需

[target.cjvm.bin-dependencies] # 仓颉二进制库依赖,适用于特定 target 的编译流程和指定该 target 作为交叉编译目标平台的编译流程,非必需
  path-option = ["./test/pro0", "./test/pro1"]
[target.cjvm.bin-dependencies.package-option]
  "pro0.xoo" = "./test/pro0/pro0.xoo.cjo"
  "pro0.yoo" = "./test/pro0/pro0.yoo.cjo"
  "pro1.zoo" = "./test/pro1/pro1.zoo.cjo"

3.条件编译

开发者可以通过预定义或自定义的条件完成条件编译;仓颉目前支持导入和声明的条件编译。

导入和声明的条件编译

仓颉支持使用内置编译标记 @When 来完成条件编译,编译条件使用 [] 括起来,[] 内支持输入一组或多组编译条件。@When 可以作用于导入节点和除 package 外的声明节点。

使用方法

以内置 os 编译条件为例,其使用方法如下:

@When[os == "Linux"]
class mc{}

main(): Int64 {
    var a = mc()
    return 0
}

在上面代码中,开发者在 Linux 系统中可以正确编译执行;在 非 Linux 系统中,则会遇到找不到 mc 类定义的编译错误。

值得注意的是:

  • 仓颉不支持编译条件嵌套,以下写法均不允许:
@When[os == "Windows"]
@When[os == "Linux"]    // Error, illegal nested when conditional compilation
import std.ast.*
@When[os == "Windows"]
@When[os == "Linux"]    // Error, illegal nested when conditional compilation
func A(){}
  • @When[…] 作为内置编译标记,在导入前处理,由宏展开生成的代码中含有 @When[…] 会编译报错,如:
@M0                     // macro which returns the input
@When[os == "Linux"]    // Error, unexpected when conditional compilation directive
func A(){}

内置编译条件变量

仓颉提供了五个内置条件变量: os、 backend、 cjc_version、 debug 和 test。

os

os 表示目标平台的操作系统。os 支持 == 和 != 两种操作符。支持的操作系统有:Windows、Linux、macOS、HarmonyOS。

使用方式如下:

@When[os == "Linux"]
func foo() {
    print("Linux, ")
}
@When[os == "Windows"]
func foo() {
    print("Windows, ")
}
@When[os != "Windows"]
func fee() {
    println("NOT Windows")
}
@When[os != "Linux"]
func fee() {
    println("NOT Linux")
}
main() {
    foo()
    fee()
}

如果在 Windows 环境下编译执行,会得到 Windows, NOT Linux 的信息;如果是在 Linux 环境下,则会得到 Linux, NOT Windows 的信息。

backend

backend 是仓颉内置的条件。仓颉是多后端语言,支持多种后端条件编译。backend 条件支持 == 和 != 两种操作符。

支持的后端有:cjnative、cjnative-x86、cjnative-x86_64、cjnative-arm、cjnative-aarch64、cjvm、cjvm-x86、cjvm-x86_64、cjvm-arm、cjvm-aarch64。

当用户使用的条件为 cjnative/cjvm 时,arch 信息将会按编译器执行时环境信息自动补全。

使用方式如下:

@When[backend == "cjnative"]
func foo() {
    print("cjnative backend, ")
}
@When[backend == "cjvm"]
func foo() {
    print("cjvm backend, ")
}
@When[backend != "cjnative"]
func fee() {
    println("NOT cjnative backend")
}
@When[backend != "cjvm"]
func fee() {
    println("NOT cjvm backend")
}
main() {
    foo()
    fee()
}

用 cjnative 后端的发布包编译执行,会得到 cjnative backend, NOT cjvm backend 的信息;用 cjvm 后端的发布包编译执行,则会得到 cjvm backend, NOT cjnative backend 的信息。

cjc_version

cjc_version 是仓颉内置的条件,开发者可以根据当前仓颉编译器的版本选择要编译的代码。cjc_version 条件支持 ==、!=、>、<、>=、<= 六种操作符,格式为 xx.xx.xx 支持每个 xx 支持 1-2 位数字,计算规则为补位 (补齐 2 位) 比较,例如:0.18.8 < 0.18.11, 0.18.8 == 0.18.08。

使用方式如下:

@When[cjc_version == "0.18.6"]
func foo() {
    println("cjc_version equals 0.18.6")
}
@When[cjc_version != "0.18.6"]
func foo() {
    println("cjc_version is NOT equal to 0.18.6")
}
@When[cjc_version > "0.18.6"]
func fnn() {
    println("cjc_version is greater than 0.18.6")
}
@When[cjc_version <= "0.18.6"]
func fnn() {
    println("cjc_version is less than or equal to 0.18.6")
}
@When[cjc_version < "0.18.6"]
func fee() {
    println("cjc_version is less than 0.18.6")
}
@When[cjc_version >= "0.18.6"]
func fee() {
    println("cjc_version is greater than or equal to 0.18.6")
}
main() {
    foo()
    fnn()
    fee()
}

根据 cjc 的版本,上面代码的执行输出结果会有不同。

debug

debug 表示当前是否启用了调试模式即开启 -g 编译选项, 可以用于在编译代码时进行调试和发布版本之间的切换。debug 条件仅支持逻辑非运算符(!)。

使用方式如下:

@When[debug]
func foo() {
    println("debug")
}
@When[!debug]
func foo() {
    println("NOT debug")
}
main() {
    foo()
}

启用 -g 编译执行会得到 cjc debug 的信息,如果没有启用 -g 编译执行会得到 NOT debug 的信息。

test

test 表示当前是否启用了单元测试选项 --test。test 条件仅支持逻辑非运算符(!)。可以用于区分测试代码与普通代码。

使用方式如下:

@When[test]
@Test
class Tests {
    @TestCase
    public func case1(): Unit {
        @Expect("run", foo())
    }
}

func foo() {
    "run"
}

@When[!test]
main () {
    println(foo())
}

使用 --test 编译执行得到的测试结果,不使用 --test 也可正常完成编译运行得到 run 的信息。

自定义编译条件变量

仓颉允许开发者自定义编译条件变量和取值,自定义的条件变量必须是一个合法的标识符且不允许和内置条件变量同名,其值是一个字符串字面量。自定义条件支持 == 和 != 两种运算符。和内置条件变量不同点在于自定义的条件需要开发者在编译时通过 --cfg 编译选项或者在配置文件 cfg.toml 中定义。

配置自定义条件变量

配置自定义条件变量的方式有两种:在编译选项中直接配置键值对或在配置文件配置键值对。

用户可以使用 --cfg <value> 以键值对的形式向编译器传递自定义编译条件变量或者指定配置文件 cfg.toml 的搜索路径。

  • 选项值需要使用双引号括起来
  • 若选项值中包含 = 则会按照键值对的形式直接进行配置(若路径中包含 = 则需要通过 \ 转义),多个键值对可以使用逗号 , 分隔。如:
$ cjc --cfg "feature = lion, platform = dsp" source.cj
  • 允许多次使用 --cfg 编译选项配置进行配置, 如:
$ cjc --cfg "feature = lion" --cfg "platform = dsp" source.cj
  • 不允许多次定义同一个条件变量, 如:
$ cjc --cfg "feature = lion" --cfg "feature = meta" source.cj
$ cjc --cfg "feature = lion, feature = meta" source.cj

上述两条编译指令都会报错。

  • 若选项值中不包含 = 或 存在通过 \ 转义的 = 则将选项值作为配置文件 cfg.toml 的搜索路径传递给编译器,如:
$ cjc --cfg "./cfg" source.cj

若 ./cfg 目录下存在 cfg.toml 则在编译时,编译器会将 ./cfg/cfg.toml 中配置的自定义编译条件传递给编译器。cfg.toml 文件中应采用键值对的方式配置自定义条件变量,每个键值对独占一行, 健名是一个合法的标识符, 键值是一个双引号括起来的字符串。如:

feature = "lion"
platform = "dsp"
  • 多次使用 --cfg 配置 cfg.toml 文件的搜索路径时,按照传入的顺序依次搜索cfg.toml 文件,若在所有传入的搜索路径下都没有找到 cfg.toml 文件,则在默认路径下搜索配置文件 cfg.toml。
  • 多次使用 --cfg 编译选项进行配置时,若某次以键值对的形式直接进行配置,则会忽略配置文件 cfg.toml 中的配置。
  • 若没有使用 --cfg 编译选项,编译器会在默认路径(通过–package 或 -p 指定的 package 目录或 cjc 执行目录)下搜索配置文件 cfg.toml。

多条件编译

仓颉条件编译允许开发者自由组合多个条件编译选项。支持逻辑运算符组合多个条件,支持括号运算符明确优先级。

使用方式如下:

//source.cj
@When[(test || feature == "lion") && !debug]
func fee() {
    println("feature lion")
}
main() {
    fee()
}

使用如下编译命令编译运行上段代码,

$ cjc --cfg="feature=lion" source.cj -o runner.out

会得到输出结果如下:

feature lion

十八、附录

1.cjc 编译选项

本章会介绍一些常用的 cjc 编译选项。若某一选项同时适用于 cjc-frontend,则该选项会有 [frontend] 上标;若该选项在 cjc-frontend 下行为与 cjc 不同,选项会有额外说明。

  • 两个横杠开头的选项为长选项,如 --xxxx。
    对于长选项,如果其后有参数,选项和参数之间既可以用空格隔开,也可以用等号连接,如 --xxxx <value> 与 --xxxx=<value> 等价。
  • 一个横杠开头的选项为短选项,如 -x。
    对于短选项,如果其后有参数,选项和参数之间可以用空格隔开,也可以不隔开,如 -x <value> 与 -x<value> 等价。

基本选项

–output-type=[exe|staticlib|dylib] [frontend]

指定输出文件的类型,exe 模式下会生成可执行文件,staticlib 模式下会生成静态库文件( .a 文件),dylib 模式下会生成动态库文件(Linux 平台为 .so 文件、Windows 平台为 .dll 文件,macOS 平台为 .dylib 文件)。

cjc 默认为 exe 模式。

我们除了可以将 .cj 文件编译成一个可执行文件以外,也可以将其编译成一个静态或者是动态的链接库,

例如使用

$ cjc tool.cj --output-type=dylib

可以将 tool.cj 编译成一个动态链接库,在 Linux 平台 cjc 会生成一个名为 libtool.so 的动态链接库文件。

[frontend] 在 cjc-frontend 中,编译流程仅进行至 LLVM IR,因此输出总是 .bc 文件,但是不同的 --output-type 类型仍会影响前端编译的策略。

–package, -p [frontend]

编译包,使用此选项时需要指定一个目录作为输入,目录中的源码文件需要属于同一个包。

假如我们有文件 log/printer.cj:

package log

public func printLog(message: String) {
    println("[Log]: ${message}")
}

与文件 main.cj:

import log.*

main() {
    printLog("Everything is great")
}

我们可以使用

$ cjc -p log --output-type=staticlib

来编译 log 包,cjc 会在当前目录下生成一个 liblog.a 文件。

然后我们可以使用 liblog.a 文件来编译 main.cj ,编译命令如下:

$ cjc main.cj liblog.a

cjc 会将 main.cj 与 liblog.a 一同编译成一个可执行文件 main 。

–module-name <value> [frontend]

指定要编译的模块的名字。

假如我们有文件 my_module/src/log/printer.cj:

package log

public func printLog(message: String) {
    println("[Log]: ${message}")
}

与文件 main.cj:

import my_module.log.*

main() {
    printLog("Everything is great")
}

我们可以使用

$ cjc -p my_module/src/log --module-name my_module --output-type=staticlib -o my_module/liblog.a

来编译 log 包并指定其模块名为 my_module,cjc 会在 my_module 目录下生成一个 my_module/liblog.a 文件。

然后我们可以使用 liblog.a 文件来编译导入了 log 包的 main.cj ,编译命令如下:

$ cjc main.cj my_module/liblog.a

cjc 会将 main.cj 与 liblog.a 一同编译成一个可执行文件 main 。

–output , -o <value>, -o<value> [frontend]

指定输出文件的路径,编译器的输出将被写入指定的文件。

例如以下命令会将输出的可执行文件名字指定为 a.out 。

cjc main.cj -o a.out
–library , -l <value>, -l<value>

指定要链接的库文件。

给定的库文件会被直接传给链接器,此编译选项一般需要和 --library-path <value> 配合使用。

文件名的格式应为 lib[arg].[extension]。当我们需要链接库 a 时,我们可以使用选项 -l a,库文件搜索目录下的 liba.a、 liba.so(或链接 Windows 目标程序时会搜索 liba.dll) 等文件会被链接器搜索到并根据需要被链接至最终输出中。

–library-path <value>, -L <value>, -L<value>

指定要链接的库文件所在的目录。

使用 --library <value> 选项时,一般也需要使用此选项来指定要链接的库文件所在的目录。

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

假如我们有从以下 C 语言源文件通过 C 语言编译器编译得到的动态库文件 libcProg.so,

#include <stdio.h>

void printHello() {
    printf("Hello World\n");
}

与仓颉文件 main.cj:

foreign func printHello(): Unit

main(): Int64 {
  unsafe {
    printHello()
  }
  return 0
}

我们可以使用

cjc main.cj -L . -l cProg

来编译 main.cj 并指定要链接的 cProg 库,这里 cjc 会输出一个可执行文件 main 。

执行 main 会有如下输出:

$ LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH ./main
Hello World

值得注意的是,由于使用了动态库文件,这里需要将库文件所在目录加入 $LD_LIBRARY_PATH 以保证 main 可以在执行时进行动态链接。

-g [frontend]

生成带有调试信息的可执行文件或者是库文件。

注意
-g 只能配合 -O0 使用,如果使用更高的优化级别可能会导致调试功能出现异常。

–trimpath <value> [frontend]

移除调试信息中源文件路径信息的前缀。

编译仓颉代码时 cjc 会保存源文件( .cj 文件)的绝对路径信息以在运行时提供调试与异常信息。

使用此选项可以将指定的路径前缀从源文件路径信息中移除,cjc 的输出文件中的源文件路径信息不会包含用户指定的部分。

可以多次使用 --trimpath 指定多个不同的路径前缀;对于每个源文件路径,编译器会将第一个匹配到的前缀从路径中移除。

–coverage [frontend]

生成支持统计代码覆盖率的可执行程序。编译器会为每一个编译单元都生成一个后缀名为 gcno 的代码信息文件。在执行程序后,每一个编译单元都会得到一个后缀名为 gcda 的执行统计文件。根据这两个文件,配合使用 cjcov 工具可以生成本次执行下的代码覆盖率报表。

注意
–coverage 只能配合 -O0 使用,如果使用更高的优化级别,编译器将告警并强制使用 -O0。–coverage 用于编译生成可执行程序,如果用于生成静态库或者动态库,那么在最终使用该库时可能出现链接错误。

–int-overflow=[throwing|wrapping|saturating] [frontend]

指定固定精度整数运算的溢出策略,默认为 throwing。

  • throwing 策略下整数运算溢出时会抛出异常
  • wrapping 策略下整数运算溢出时会回转至对应固定精度整数的另外一端
  • saturating 策略下整数运算溢出时会选择对应固定精度的极值作为结果
–diagnostic-format=[default|noColor|json] [frontend]

注意
Windows 版本暂不支持输出带颜色渲染的错误信息。

指定错误信息的输出格式,默认为 default 。

  • default 错误信息默认格式输出(带颜色)
  • noColor 错误信息默认格式输出(无颜色)
  • json 错误信息json格式输出
–verbose, -V [frontend]

cjc 会打印出编译器版本信息,工具链依赖的相关信息以及编译过程中执行的命令。

–help, -h [frontend]

打印可用的编译选项。

使用此选项时编译器仅会打印编译选项相关信息,不会对任何输入文件进行编译。

–version, -v [frontend]

打印编译器版本信息。

使用此选项时编译器仅会打印版本信息,不会对任何输入文件进行编译。

–save-temps <value>

保留编译过程中生成的中间文件并保存至 路径下。

编译器会保留编译过程中生成的 .bc, .o 等中间文件。

–import-path <value> [frontend]

指定导入模块的 AST 文件的搜索路径。

假如我们已经有以下目录结构,libs/myModule 目录中包含 myModule 模块的库文件和 log 包的 AST 导出文件,

.
├── libs
|   └── myModule
|       ├── log.cjo
|       └── libmyModule.a
└── main.cj

且我们有代码如下的 main.cj 文件,

import myModule.log.printLog

main() {
    printLog("Everything is great")
}

我们可以通过使用 --import-path ./libs 来将 ./libs 加入导入模块的 AST 文件搜索路径,cjc 会使用 ./libs/myModule/log.cjo 文件来对 main.cj 文件进行语义检查与编译。

–import-path 提供与 CANGJIE_PATH 环境变量相同的功能,但通过 --import-path 设置的路径拥有更高的优先级。

–scan-dependency [frontend]

通过 --scan-dependency 指令可以获得指定包源码或者一个包的 cjo 文件对于其他包的直接依赖以及其他信息,以 json 格式输出。

// this file is placed under directory pkgA
macro package pkgA
import pkgB.*
import std.io.*
import pkgB.subB.*
cjc --scan-dependency --package pkgA

cjc --scan-dependency pkgA.cjo
{
  "package": "pkgA",
  "isMacro": true,
  "dependencies": [
    {
      "package": "pkgB",
      "isStd": false,
      "imports": [
        {
          "file": "pkgA/pkgA.cj",
          "begin": {
            "line": 2,
            "column": 1
          },
          "end": {
            "line": 2,
            "column": 14
          }
        }
      ]
    },
    {
      "package": "pkgB.subB",
      "isStd": false,
      "imports": [
        {
          "file": "pkgA/pkgA.cj",
          "begin": {
            "line": 4,
            "column": 1
          },
          "end": {
            "line": 4,
            "column": 19
          }
        }
      ]
    },
    {
      "package": "std.io",
      "isStd": true,
      "imports": [
        {
          "file": "pkgA/pkgA.cj",
          "begin": {
            "line": 3,
            "column": 1
          },
          "end": {
            "line": 3,
            "column": 16
          }
        }
      ]
    }
  ]
}
–no-sub-pkg [frontend]

表明当前编译包没有子包。

开启该选项后,编译器可以进一步缩减 code size 大小。

–warn-off, -Woff <value> [frontend]

关闭编译期出现的全部或部分警告。

<value> 可以为 all 或者一个设定好的警告组别。当参数为 all 时,对于编译过程中生成的所有警告,编译器都不会打印;当参数为其他设定好的组别时,编译器将不会打印编译过程中生成的该组别警告。

在打印每个警告时,会有一行 #note 提示该警告属于什么组别并如何关闭它,我们可以通过 --help 打印所有可用的编译选项参数,来查阅具体的组别名称。

–warn-on, -Won <value> [frontend]

开启编译期出现的全部或部分警告。

–warn-on 的 <value> 与 --warn-off 的 <value> 取值范围相同,–warn-on 通常与 --warn-off 组合使用;比如,我们可以通过设定 -Woff all -Won <value> 来仅允许组别为 <value> 的警告被打印。

特别要注意的是,–warn-on 与 --warn-off 在使用上顺序敏感;针对同一组别,后设定的选项会覆盖之前选项的设定,比如,调换上例中两个编译选项的位置,使其变为 -Won <value> -Woff all,其效果将变为关闭所有警告。

–error-count-limit <value> [frontend]

限制编译器打印错误个数的上限。

参数 <value> 可以为 all 或一个非负整数。当参数为 all 时,编译器会打印编译过程中生成的所有错误;当参数为非负整数 N 时,编译器最多会打印 N 个错误。此选项默认值为 8。

–output-dir <value> [frontend]

控制编译器生成的中间文件与最终文件的保存目录。

控制编译器生成的中间文件的保存目录,例如 .cjo 文件。当指定 --output-dir <path1> 时也指定了 --output <path2>,则中间文件会被保存至 <path1>,最终输出会被保存至 <path1>/<path2>

注意
同时指定此选项与 --output 选项时,–output 选项的参数必须是一个相对路径。

–static-std

静态链接仓颉库的 std 模块。

此选项仅在编译动态链接库或可执行文件时生效。cjc 默认静态链接仓颉库的 std 模块。

–dy-std

动态链接仓颉库的 std 模块。

此选项仅在编译动态链接库或可执行文件时生效。

–static-libs

静态链接仓颉库非 std 的其他模块。

此选项仅在编译动态链接库或可执行文件时生效。cjc 默认静态链接仓颉库的非 std 的其他模块。

值得注意的是:

  1. –static-std 和 --dy-std 选项一起叠加使用,仅最后的那个选项生效;
  2. –dy-std 与 --static-libs选项不可一起使用,否则会报错。
–dy-libs

动态链接仓颉库非 std 的其他模块。

此选项仅在编译动态链接库或可执行文件时生效。

值得注意的是:

  1. –static-libs 和 --dy-libs 选项一起叠加使用,仅最后的那个选项生效;
  2. –static-std 与 --dy-libs 选项不可一起使用,否则会报错;
  3. –dy-std 单独使用时,会默认生效 --dy-libs 选项,并有相关告警信息提示;
  4. –dy-libs 单独使用时,会默认生效 --dy-std 选项,并有相关告警信息提示。
–stack-trace-format=[default|simple|all]

指定异常调用栈打印格式,用来控制异常抛出时的栈帧信息显示,默认为 default 格式。

异常调用栈的格式说明如下:

  • default 格式:省略泛型参数的函数名 (文件名:行号)
  • simple 格式: 文件名:行号
  • all 格式:完整的函数名 (文件名:行号)
–lto=[full|thin]

使能且指定 LTO (Link Time Optimization 链接时优化)优化编译模式。

值得注意的是:

  1. 支持编译可执行文件和 LTO 模式下的静态库(.bc 文件),不支持编译生成动态库,即如果在 LTO 模式下指定 --output-type=dylib 则会编译报错;
  2. Windows 以及 macOS 平台不支持该功能;
  3. 当使能且指定 LTO (Link Time Optimization 链接时优化)优化编译模式时,不允许同时使用如下优化编译选项:-Os、-Oz。

LTO 优化支持两种编译模式:

  • –lto=full:full LTO 将所有编译模块合并到一起,在全局上进行优化,这种方式可以获得最大的优化潜力,同时也需要更长的编译时间。

  • –lto=thin:相比于 full LTO,thin LTO 在多模块上使用并行优化,同时默认支持链接时增量编译,编译时间比 full LTO 短,因为失去了更多的全局信息,所以优化效果不如 full LTO。

    • 通常情况下优化效果对比:full LTO > thin LTO > 常规静态链接编译。
    • 通常情况下编译时间对比:full LTO > thin LTO > 常规静态链接编译。

LTO 优化使用场景:

  1. 使用以下命令编译可执行文件
$ cjc test.cj --lto=full
or
$ cjc test.cj --lto=thin
  1. 使用以下命令编译 LTO 模式下需要的静态库(.bc 文件),并且使用该库文件参与可执行文件编译
# 生成的静态库为 .bc 文件
$ cjc pkg.cj --lto=full --output-type=staticlib -o libpkg.bc
# .bc 文件和源文件一起输入给仓颉编译器编译可执行文件
$ cjc test.cj libpkg.bc --lto=full

注意
LTO 模式下的静态库(.bc 文件)输入的时候需要将该文件的路径输入仓颉编译器。

  1. 在 LTO 模式下,静态链接标准库(–static-std & -static-libs)时,标准库的代码也会参与 LTO 优化,并静态链接到可执行文件;动态链接标准库(–dy-std & -dy-libs)时,在 LTO 模式下依旧使用标准库中的动态库参与链接。
# 静态链接,标准库代码也参与 LTO 优化
$ cjc test.cj --lto=full --static-std
# 动态链接,依旧使用动态库参与链接,标准库代码不会参与 LTO 优化
$ cjc test.cj --lto=full --dy-std
–pgo-instr-gen

使能插桩编译,生成携带插桩信息的可执行程序。

编译 macOS 与 Windows 目标时暂不支持使用该功能。

PGO (全称Profile-Guided Optimization)是一种常用编译优化技术,通过使用运行时 profiling 信息进一步提升程序性能。Instrumentation-based PGO 是使用插桩信息的一种 PGO 优化手段,它通常包含三个步骤:

  1. 编译器对源码插桩编译,生成插桩后的可执行程序(instrumented program);
  2. 运行插桩后的可执行程序,生成配置文件;
  3. 编译器使用配置文件,再次对源码进行编译。
# 生成支持源码执行信息统计(携带插桩信息)的可执行程序 test
$ cjc test.cj --pgo-instr-gen -o test
# 运行可执行程序 test 结束后,生成 test.profraw 配置文件
$ LLVM_PROFILE_FILE="test.profraw" ./test

注意
运行程序时使用环境变量 LLVM_PROFILE_FILE=“test%c.profraw” 可开启连续模式,即在程序崩溃或被信号杀死的情况下也能生成配置文件,可使用 llvm-profdata 工具对其进行查看分析。但是,目前 PGO 不支持连续模式下进行后续的优化步骤。

–pgo-instr-use=<.profdata>

使用指定 profdata 配置文件指导编译并生成优化后的可执行程序。

编译 macOS 目标时暂不支持使用该功能。

注意
–pgo-instr-use 编译选项仅支持格式为 profdata 的配置文件。可使用 llvm-profdata 工具可将 profraw 配置文件转换为 profdata 配置文件。

# 将 `profraw` 文件转换为 `profdata` 文件。
$ LD_LIBRARY_PATH=$CANGJIE_HOME/third_party/llvm/lib:$LD_LIBRARY_PATH $CANGJIE_HOME/third_party/llvm/bin/llvm-profdata merge test.profraw -o test.profdata
# 使用指定 `test.profdata` 配置文件指导编译并生成优化后的可执行程序 `testOptimized`
$ cjc test.cj --pgo-instr-use=test.profdata -o testOptimized
–target [frontend]

指定编译的目标平台的 triple。

参数 <value> 一般为符合以下格式的字符串:<arch>(-<vendor>)-<os>(-<env>)。其中:

  • <arch> 表示目标平台的系统架构,例如 aarch64,x86_64 等;
  • <vendor> 表示开发目标平台的厂商,常见的例如 pc,apple 等,在没有明确平台厂商或厂商不重要的情况下也经常写作 unknown 或直接省略;
  • <os> 表示目标平台的操作系统,例如 Linux,Win32 等;
  • <env> 表示目标平台的 ABI 或标准规范,用于更细粒度地区分同一操作系统的不同运行环境,例如 gnu,musl 等。在操作系统不需要根据 <env> 进行更细地区分的时候,此项也可以省略。

目前,cjc 已支持交叉编译的本地平台和目标平台如下表所示:

本地平台 (host)目标平台 (target)
x86_64-windows-gnuaarch64-linux-ohos
x86_64-apple-darwinaarch64-linux-ohos

在使用 --target 指定目标平台进行交叉编译之前,请准备好对应目标平台的交叉编译工具链,以及可以在本地平台上运行的、向该目标平台编译的对应 Cangjie SDK 版本。

–target-cpu <value>

注意
该选项为实验性功能,使用该功能生成的二进制有可能会存在潜在的运行时问题,请注意使用该选项的风险。此选项必须配合 --experimental 选项一同使用。
指定编译目标的 CPU 类型。

指定编译目标的 CPU 类型时,编译器在生成二进制时会尝试使用该 CPU 类型特有的扩展指令集,并尝试应用适用于该 CPU 类型的优化。为某个特定 CPU 类型生成的二进制通常会失去可移植性,该二进制可能无法在其他(拥有相同架构指令集的)CPU 上运行。

该选项支持以下经过测试的 CPU 类型:

x86-64 架构:

  • generic

aarch64 架构:

generic
tsv110
generic 为通用 CPU 类型,指定 generic 时编译器会生成适用于该架构的通用指令,这样生成的二进制在操作系统和二进制本身的动态依赖一致的前提下,可以在基于该架构的各种 CPU 上运行,无关于具体的 CPU 类型。–target-cpu 选项的默认值为 generic。

该选项还支持以下 CPU 类型,但以下 CPU 类型未经过测试验证,请注意使用以下 CPU 类型生成的二进制可能会存在运行时问题。

x86-64 架构:

  • alderlake
  • amdfam10
  • athlon
  • athlon-4
  • athlon-fx
  • athlon-mp
  • athlon-tbird
  • athlon-xp
  • athlon64
  • athlon64-sse3
  • atom
  • barcelona
  • bdver1
  • bdver2
  • bdver3
  • bdver4
  • bonnell
  • broadwell
  • btver1
  • btver2
  • c3
  • c3-2
  • cannonlake
  • cascadelake
  • cooperlake
  • core-avx-i
  • core-avx2
  • core2
  • corei7
  • corei7-avx
  • geode
  • goldmont
  • goldmont-plus
  • haswell
  • i386
  • i486
  • i586
  • i686
  • icelake-client
  • icelake-server
  • ivybridge
  • k6
  • k6-2
  • k6-3
  • k8
  • k8-sse3
  • knl
  • knm
  • lakemont
  • nehalem
  • nocona
  • opteron
  • opteron-sse3
  • penryn
  • pentium
  • pentium-m
  • pentium-mmx
  • pentium2
  • pentium3
  • pentium3m
  • pentium4
  • pentium4m
  • pentiumpro
  • prescott
  • rocketlake
  • sandybridge
  • sapphirerapids
  • silvermont
  • skx
  • skylake
  • skylake-avx512
  • slm
  • tigerlake
  • tremont
  • westmere
  • winchip-c6
  • winchip2
  • x86-64
  • x86-64-v2
  • x86-64-v3
  • x86-64-v4
  • yonah
  • znver1
  • znver2
  • znver3

aarch64 架构:

  • a64fx
  • ampere1
  • apple-a10
  • apple-a11
  • apple-a12
  • apple-a13
  • apple-a14
  • apple-a7
  • apple-a8
  • apple-a9
  • apple-latest
  • apple-m1
  • apple-s4
  • apple-s5
  • carmel
  • cortex-a34
  • cortex-a35
  • cortex-a510
  • cortex-a53
  • cortex-a55
  • cortex-a57
  • cortex-a65
  • cortex-a65ae
  • cortex-a710
  • cortex-a72
  • cortex-a73
  • cortex-a75
  • cortex-a76
  • cortex-a76ae
  • cortex-a77
  • cortex-a78
  • cortex-a78c
  • cortex-r82
  • cortex-x1
  • cortex-x1c
  • cortex-x2
  • cyclone
  • exynos-m3
  • exynos-m4
  • exynos-m5
  • falkor
  • kryo
  • neoverse-512tvb
  • neoverse-e1
  • neoverse-n1
  • neoverse-n2
  • neoverse-v1
  • saphira
  • thunderx
  • thunderx2t99
  • thunderx3t110
  • thunderxt81
  • thunderxt83
  • thunderxt88
    除以上可选 CPU 类型,该选项可以使用 native 作为当前 CPU 类型,编译器会尝试识别当前机器的 CPU 类型并使用该 CPU 类型作为目标类型生成二进制。
–toolchain <value>, -B <value>, -B<value>

指定编译工具链中,二进制文件存放的路径。

二进制文件包括:编译器、链接器、工具链等提供的 C 运行时目标文件(例如 crt0.o、 crti.o等)。

我们在准备好编译工具链后,可以在将其存放在一个自定义路径,然后通过 --toolchain <value> 向编译器传入该路径,即可让编译器调用到该路径下的二进制文件进行交叉编译。

–sysroot <value>

指定编译工具链的根目录路径。

对于目录结构固定的交叉编译工具链,如果我们没有指定该目录以外的二进制和动态库、静态库文件路径的需求,可以直接使用 --sysroot <value> 向编译器传入工具链的根目录路径,编译器会根据目标平台种类分析对应的目录结构,自动搜索所需的二进制文件和动态库、静态库文件。使用该选项后,我们无需再指定 --toolchain、–library-path 参数。

假如我们向 triple 为 arch-os-env 的平台进行交叉编译,同时我们的交叉编译工具链有以下目录结构:

/usr/sdk/arch-os-env
├── bin
|   ├── arch-os-env-gcc (交叉编译器)
|   ├── arch-os-env-ld  (链接器)
|   └── ...
├── lib
|   ├── crt1.o          (C 运行时目标文件)
|   ├── crti.o
|   ├── crtn.o
|   ├── libc.so         (动态库)
|   ├── libm.so
|   └── ...
└── ...

我们有仓颉源文件 hello.cj ,那么我们可以使用以下命令,将 hello.cj 交叉编译至 arch-os-env 平台:

cjc --target=arch-os-env --toolchain /usr/sdk/arch-os-env/bin --toolchain /usr/sdk/arch-os-env/lib --library-path /usr/sdk/arch-os-env/lib hello.cj -o hello

也可以使用简写的参数:

cjc --target=arch-os-env -B/usr/sdk/arch-os-env/bin -B/usr/sdk/arch-os-env/lib -L/usr/sdk/arch-os-env/lib hello.cj -o hello

如果该工具链的目录符合惯例的目录结构,也可以无需使用 --toolchain、–library-path 参数,而使用以下的命令:

cjc --target=arch-os-env --sysroot /usr/sdk/arch-os-env hello.cj -o hello
–strip-all, -s

编译可执行文件或动态库时,指定该选项以删除输出文件中的符号表。

–discard-eh-frame

编译可执行文件或动态库时,指定该选项可以删除 eh_frame 段以及 eh_frame_hdr 段中的部分信息(涉及到 crt 的相关信息不作处理),减少可执行文件或动态库的大小,但会影响调试信息。

编译 macOS 目标时暂不支持使用该功能。

–link-options <value>1

指定链接器选项。

cjc 会将该选项的参数透传给链接器。可用的参数会因(系统或指定的)链接器的不同而不同。可以多次使用 --link-options 指定多个链接器选项。

1 上标表示链接器透传选项可能会因为链接器的不同而不同,具体支持的选项请查阅链接器文档。

单元测试选项

–test [frontend]

unittest 测试框架提供的入口,由宏自动生成,当使用 cjc --test 选项编译时,程序入口不再是 main,而是 test_entry。unittest 测试框架的使用方法请参见 《仓颉编程语言库 API》文档。

对于 pkgc 目录下仓颉文件 a.cj:

import std.unittest.*
import std.unittest.testmacro.*

@Test
public class TestA {
    @TestCase
    public func case1(): Unit {
        print("case1\n")
    }
}

我们可以在 pkgc 目录下使用:

cjc a.cj --test

来编译 a.cj ,执行 main 会有如下输出:

注意
不保证用例每次执行的用时都相同。

case1
--------------------------------------------------------------------------------------------------
TP: default, time elapsed: 29710 ns, Result:
    TCS: TestA, time elapsed: 26881 ns, RESULT:
    [ PASSED ] CASE: case1 (16747 ns)
Summary: TOTAL: 1
    PASSED: 1, SKIPPED: 0, ERROR: 0
    FAILED: 0
--------------------------------------------------------------------------------------------------

对于如下目录结构 :

application
├── src
├── pkgc
|   ├── a1.cj
|   └── a2.cj
└── a3.cj

我们可以在 application目录下使用 -p 编译选项配合编译整包:

cjc pkgc --test -p

来编译整个 pkgc 包下的测试用例 a1.cj 和 a2.cj。

/*a1.cj*/
package a

import std.unittest.*
import std.unittest.testmacro.*

@Test
public class TestA {
    @TestCase
    public func caseA(): Unit {
        print("case1\n")
    }
}
/*a2.cj*/
package a

import std.unittest.*
import std.unittest.testmacro.*

@Test
public class TestB {
    @TestCase
    public func caseB(): Unit {
        throw IndexOutOfBoundsException()
    }
}

执行 main 会有如下输出(输出信息仅供参考):

case1
--------------------------------------------------------------------------------------------------
TP: a, time elapsed: 367800 ns, Result:
    TCS: TestA, time elapsed: 16802 ns, RESULT:
    [ PASSED ] CASE: caseA (14490 ns)
    TCS: TestB, time elapsed: 347754 ns, RESULT:
    [ ERROR  ] CASE: caseB (345453 ns)
    REASON: An exception has occurred:IndexOutOfBoundsException
        at std/core.Exception::init()(std/core/exception.cj:23)
        at std/core.IndexOutOfBoundsException::init()(std/core/index_out_of_bounds_exception.cj:9)
        at a.TestB::caseB()(/home/houle/cjtest/application/pkgc/a2.cj:7)
        at a.lambda.1()(/home/houle/cjtest/application/pkgc/a2.cj:7)
        at std/unittest.TestCases::execute()(std/unittest/test_case.cj:92)
        at std/unittest.UT::run(std/unittest::UTestRunner)(std/unittest/test_runner.cj:194)
        at std/unittest.UTestRunner::doRun()(std/unittest/test_runner.cj:78)
        at std/unittest.UT::run(std/unittest::UTestRunner)(std/unittest/test_runner.cj:200)
        at std/unittest.UTestRunner::doRun()(std/unittest/test_runner.cj:78)
        at std/unittest.UT::run(std/unittest::UTestRunner)(std/unittest/test_runner.cj:200)
        at std/unittest.UTestRunner::doRun()(std/unittest/test_runner.cj:75)
        at std/unittest.entryMain(std/unittest::TestPackage)(std/unittest/entry_main.cj:11)
Summary: TOTAL: 2
    PASSED: 1, SKIPPED: 0, ERROR: 1
    FAILED: 0
--------------------------------------------------------------------------------------------------
–mock <on|off|runtime-error> [frontend]

如果传递了 on ,则该包将使能 mock 编译,该选项允许在测试用例中 mock 该包中的类。off 是一种显式禁用 mock 的方法。

值得注意的是,在测试模式下(当使能 --test )自动启用对此包的 mock 支持,不需要显式传递 --mock 选项。

runtime-error 仅在测试模式下可用(当使能 --test 时),它允许编译带有 mock 代码的包,但不在编译器中做任何 mock 相关的处理(这些处理可能会造成一些开销并影响测试的运行时性能)。这对于带有 mock 代码用例进行基准测试时可能是有用的。使用此编译选项时,避免编译带有 mock 代码的用例并运行测试,否则将抛出运行时异常。

宏选项

cjc 支持以下宏选项,关于宏的更多内容请参阅宏章节。

–compile-macro [frontend]

编译宏定义文件,生成默认的宏定义动态库文件。

–debug-macro [frontend]

生成宏展开后的仓颉代码文件。该选项可用于调试宏展开功能。

–parallel-macro-expansion [frontend]

开启宏展开并行。该选项可用于缩短宏展开编译时间。

条件编译选项

cjc 支持以下条件编译选项,关于条件编译的更多内容请参阅“条件编译”。

–cfg [frontend]

指定自定义编译条件。

并行编译选项

cjc 支持以下并行编译选项以获得更高的编译效率。

–jobs <value>, -j <value> [frontend]

设置并行编译时所允许的最大并行数。其中 value 必须是一个合理的正整数,当 value 大于硬件支持最大并行能力时,编译器将会按基于硬件支持并行能力计算出的默认设置执行并行编译。

如果该编译选项未设置,编译器将会按基于硬件支持并行能力计算出的默认设置执行并行编译。

注意
–jobs 1表示完全使用串行方式进行编译。

–aggressive-parallel-compile, --apc [frontend]

开启此选项后,编译器会采用更加激进的策略(可能会对优化造成影响)执行并行编译,以便获得更高的编译效率。

注意
–aggressive-parallel-compile选项在一些场景下会由编译器强制开启/关闭。

在以下场景中–aggressive-parallel-compile选项将由编译器强制开启:

  • -O0
  • -g

在以下场景中–aggressive-parallel-compile选项将由编译器强制关闭:

  • –fobf-string
  • –fobf-const
  • –fobf-layout
  • –fobf-cf-flatten
  • –fobf-cf-bogus
  • –lto
  • –coverage
  • 编译 Windows 目标
  • 编译 macOS 目标

优化选项

–fchir-constant-propagation [frontend]

开启 chir 常量传播优化。

–fno-chir-constant-propagation [frontend]

关闭 chir 常量传播优化。

–fchir-function-inlining [frontend]

开启 chir 函数内联优化。

–fno-chir-function-inlining [frontend]

关闭 chir 函数内联优化。

–fchir-devirtualization [frontend]

开启 chir 去虚函数调用优化。

–fno-chir-devirtualization [frontend]

关闭 chir 去虚函数调用优化。

–fast-math [frontend]

开启此选项后,编译器会对浮点数作一些激进且有可能损失精度的假设,以便优化浮点数运算。

-O<N> [frontend]

使用参数指定的代码优化级别。

指定越高的优化级别,编译器会越多地进行代码优化以生成更高效的程序,同时也可能会需要更长的编译时间。

cjc 默认使用 O0 级别的代码优化。当前 cjc 支持如下优化级别:O0、O1、O2、Os、Oz。

当优化等级等于 2 时,cjc 除了进行对应的优化外,还会开启以下选项:

  • –fchir-constant-propagation
  • –fchir-function-inlining
  • –fchir-devirtualization

当优化等级等于 s 时, cjc除了进行 O2 级别优化外,将针对 code size 进行优化。

当优化等级等于 z 时, cjc除了进行 Os 级别优化外,还将进一步缩减 code size 大小。

注意
当优化等级等于 s 或 z 时,不允许同时使用链接时优化编译选项 --lto=[full|thin]。

-O [frontend]

使用 O1 级别的代码优化,等价于 -O1。

代码混淆选项

cjc 支持代码混淆功能以提供对代码的额外安全保护,代码混淆功能默认不开启。

cjc 支持以下代码混淆选项:

–fobf-string

开启字符串混淆。

混淆代码中出现的字符串常量,攻击者无法静态直接读取二进制程序中的字符串数据。

–fno-obf-string

关闭字符串混淆。

–fobf-const

开启常量混淆。

混淆代码中使用的数值常量,将的数值运算指令替换成等效的、更复杂的数值运算指令序列。

–fno-obf-const

关闭常量混淆。

–fobf-layout

开启外形混淆。

外形混淆功能会混淆代码中的符号(包括函数名和全局变量名)、路径名、代码行号和函数排布顺序。使用该编译选项后,cjc 会在当前目录生成符号映射输出文件 *.obf.map,如果配置了 --obf-sym-output-mapping 选项,则 --obf-sym-output-mapping 的参数值将作为 cjc 生成的符号映射输出文件名。符号映射输出文件中包含混淆前后符号的映射关系,使用符号映射输出文件我们可以解混淆被混淆过的符号。

注意
外形混淆功能和并行编译功能相互冲突,请勿同时开启。如果和并行编译同时开启,并行编译将失效。

–fno-obf-layout

关闭外形混淆。

–obf-sym-prefix <string>

指定外形混淆功能在混淆符号时添加的前缀字符串。

设置该选项后,所有被混淆符号都会加上该前缀。在编译混淆多个仓颉包时可能出现符号冲突的问题,可以使用该选项给不同的包指定不同的前缀,避免符号冲突。

–obf-sym-output-mapping <file>

指定外形混淆的符号映射输出文件。

符号映射输出文件记录了符号的原始名称、混淆后的名称和所属文件路径。使用符号映射输出文件我们可以解混淆被混淆过的符号。

–obf-sym-input-mapping <file,…>

指定外形混淆的符号映射输入文件。

外形混淆功能会使用这些文件中的映射关系对符号进行混淆。因此在编译存在调用关系的仓颉包,请使用被调用包的符号映射输出文件作为调用包混淆时的 --obf-sym-input-mapping 选项的参数,以此保证同一个符号在调用包和被调用包两者混淆时混淆结果一致。

–obf-apply-mapping-file <file>

提供自定义的外形混淆符号映射关系文件,外形混淆功能将按照文件里的映射关系混淆符号。

文件格式如下:

<original_symbol_name> <new_symbol_name>

其中 original_symbol_name 是混淆前的名称,new_symbol_name 是混淆后的名称。original_symbol_name 由多个 field 组成。field 表示字段名,可以是模块名、包名、类名、结构体名、枚举名、函数名或变量名。field 之间用分隔符 ‘.’ 分隔。如果 field 是函数名,则需要将函数的参数类型用括号 ‘()’ 修饰并附加在函数名后面。对于无参函数括号内的内容为空。如果 field 存在泛型参数,也需要用括号 ‘<>’ 将具体的泛型参数附加在 field 后面。

外形混淆功能会将仓颉应用中的 original_symbol_name 替换为 new_symbol_name。对于不在该文件中的符号,外形混淆功能会正常使用随机名称进行替换。如果该文件中指定的映射关系和 --obf-sym-input-mapping 中的映射关系相冲突,编译器会抛出异常并停止编译。

–fobf-export-symbols

允许外形混淆功能混淆导出符号,该选项在开启外形混淆功能时默认开启。

开启该选项后,外形混淆功能会对导出符号进行混淆。

–fno-obf-export-symbols

禁止外形混淆功能混淆导出符号。

–fobf-source-path

允许外形混淆功能混淆符号的路径信息,该选项在开启外形混淆功能时默认开启。

开启该选项后,外形混淆功能会混淆异常堆栈信息中的路径信息,将路径名替换为字符串 “SOURCE”。

–fno-obf-source-path

禁止外形混淆功能混淆堆栈信息中的路径信息。

–fobf-line-number

允许外形混淆功能混淆堆栈信息中的行号信息,该选项在开启外形混淆功能时默认开启。

开启该选项后,外形混淆功能会混淆异常堆栈信息中的行号信息,将行号替换为 0。

–fno-obf-line-number

禁止外形混淆功能混淆堆栈信息中的行号信息。

–fobf-cf-flatten

开启控制流平坦化混淆。

混淆代码中既存的控制流,使其转移逻辑变得复杂。

–fno-obf-cf-flatten

关闭控制流平坦化混淆。

–fobf-cf-bogus

开启虚假控制流混淆。

在代码中插入虚假的控制流,使代码逻辑变得复杂。

–fno-obf-cf-bogus

关闭虚假控制流混淆。

–fobf-all

开启所有混淆功能。

指定该选项等同于同时指定以下选项:

  • –fobf-string

  • –fobf-const

  • –fobf-layout

  • –fobf-cf-flatten

  • –fobf-cf-bogus

–obf-config <file>

指定代码混淆配置文件路径。

在配置文件中我们可以禁止混淆工具对某些函数或者符号进行混淆。

配置文件的具体格式如下:

obf_func1 name1
obf_func2 name2
...

第一个参数 obf_func 是具体的混淆功能:

  • obf-cf-bogus:虚假控制流混淆
  • obf-cf-flatten:控制流平坦化混淆
  • obf-const:常数混淆
  • obf-layout:外形混淆

第二个参数 name 是需要被保留的对象,由多个 field 组成。field 表示字段名,可以是包名、类名、结构体名、枚举名、函数名或变量名。

field 之间用分隔符 ‘.’ 分隔。如果 field 是函数名,则需要将函数的参数类型用括号 ‘()’ 修饰并附加在函数名后面。对于无参函数括号内的内容为空。

比如,假设在包 packA 中有以下代码:

package packA
class MyClassA {
    func funcA(a: String, b: Int64): String {
        return a
    }
}

如果要禁止控制流平坦化功能混淆 funcA,用户可以编写如下规则:

obf-cf-flatten packA.MyClassA.funcA(std.core.String, Int64)

用户也可以使用通配符编写更加灵活的规则,达到一条规则保留多个对象的目的。目前支持的通配符包含以下 3 类:

混淆功能通配符:

混淆功能通配符说明
?匹配名称中的单个字符
?匹配名称中的任意数量字符

字段名通配符:

字段名通配符说明
?匹配字段名中单个非分隔符 ‘.’ 的字符
*匹配字段名中的不包含分隔符 ‘.’ 和参数的任意数量字符
**匹配字段名中的任意数量字符,包括字段之间的分隔符 ‘.’ 和参数。‘**’ 只有在单独作为一个 field 时才生效,否则会被当作 ‘*’ 处理

函数的参数类型通配符:

参数类型通配符说明
匹配任意数量的参数
***匹配一个任意类型的参数

说明
参数类型也由字段名组成,因此也可以使用字段名通配符对单个参数类型进行匹配。

以下是通配符使用示例:

例子 1:

obf-cf-flatten pro?.myfunc()

该规则表示禁止 obf-cf-flatten 功能混淆函数 pro?.myfunc(),pro?.myfunc() 可以匹配 pro0.myfunc(),但不能匹配 pro00.myfunc()。

例子 2:

* pro0.**

该规则表示禁止任何混淆功能混淆包 pro0 下的任何函数和变量。

例子 3:

* pro*.myfunc(...)

该规则表示禁止任何混淆功能混淆函数 pro*.myfunc(…),pro*.myfunc(…) 可以匹配以 pro 开头的任意单层包内的 myfunc 函数,且可以为任意参数。

如果需要匹配多层包名,比如 pro0.mypack.myfunc(),请使用 pro*.**.myfunc(...)。请注意 '**' 只有单独作为字段名时才生效,因此 pro**.myfunc(…) 和 pro*.myfunc(…) 等价,无法匹配多层包名。如果要匹配以 pro 开头的所有包下的所有 myfunc 函数(包括类中名为 myfunc 的函数),请使用 pro*.**.myfunc(…)。

例子 4:

obf-cf-* pro0.MyClassA.myfunc(**.MyClassB, ***, ...)

该规则表示禁止 obf-cf-* 功能混淆函数 pro0.MyClassA.myfunc(**.MyClassB, ***, ...),其中 obf-cf-* 会匹配 obf-cf-bogus 和 obf-cf-flatten 两种混淆功能,pro0.MyClassA.myfunc(**.MyClassB, ***, ...) 会匹配函数 pro0.MyClassA.myfunc,且函数的第一个参数可以是任意包下的 MyClassB 类型,第二个参数可以是任意类型,后面可以接零至多个任意参数。

–obf-level <value>

指定混淆功能强度级别。

可指定 1-9 强度级别。默认强度级别为 5。级别数字越大,强度则越高,该选项会影响输出文件的大小以及执行开销。

–obf-seed <value>

指定混淆算法的随机数种子。

通过指定混淆算法的随机数种子,我们可以使同一份仓颉代码在不同构建时有不同的混淆结果。默认场景下,对于同一份仓颉代码,在每次混淆后都拥有相同的混淆结果。

安全编译选项

注意
Windows 以及 macOS 版本暂不支持安全编译选项。
cjc 默认生成地址无关代码,在编译可执行文件时默认生成地址无关可执行文件。

cjc 支持通过 --link-options 设置以下安全相关的链接器选项:

–link-options "-z noexecstack"1

设置线程栈不可执行。

–link-options "-z relro"1

设置 GOT 表重定位只读。

–link-options "-z now"1

设置立即绑定。

代码覆盖率插桩选项

注意
Windows 以及 macOS 版本暂不支持代码覆盖率插桩选项。

仓颉支持对代码覆盖率插桩(SanitizerCoverage,以下简称 SanCov),提供与 LLVM 的 SanitizerCoverage 一致的接口,编译器在函数级或 BasicBlock 级插入覆盖率反馈函数,用户只需要实现约定好的回调函数即可在运行过程中感知程序运行状态。

仓颉提供的 SanCov 功能以 package 为单位,即整个 package 只有全部插桩和全部不插桩两种情况。

–sanitizer-coverage-level=0/1/2

插桩等级:0 表示不插桩;1 表示函数级插桩,只在函数入口处插入回调函数;2 表示 BasicBlock 级插桩,在各个 BasicBlock 处插入回调函数。

如不指定,默认值为 2。

该编译选项只影响 --sanitizer-coverage-trace-pc-guard、–sanitizer-coverage-inline-8bit-counters、–sanitizer-coverage-inline-bool-flag 的插桩等级。

–sanitizer-coverage-trace-pc-guard

开启该选项,会在每个 Edge 插入函数调用 __sanitizer_cov_trace_pc_guard(uint32_t *guard_variable),受 sanitizer-coverage-level 影响。

值得注意的是,该功能存在与 gcc/llvm 实现不一致的地方:不会在 constructor 插入 void __sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop),而是在 package 初始化阶段插入函数调用 uint32_t *__cj_sancov_pc_guard_ctor(uint64_t edgeCount)。

__cj_sancov_pc_guard_ctor 回调函数需要开发者自行实现,开启 SanCov 的 package 会尽可能早地调用该回调函数,入参是该 Package 的 Edge 个数,返回值是通常是 calloc 创建的内存区域。

如果需要调用 __sanitizer_cov_trace_pc_guard_init,建议在 __cj_sancov_pc_guard_ctor 中调用,使用动态创建的缓冲区计算该函数的入参和返回值。

一个标准的__cj_sancov_pc_guard_ctor参考实现如下:

uint32_t *__cj_sancov_pc_guard_ctor(uint64_t edgeCount) {
    uint32_t *p = (uint32_t *) calloc(edgeCount, sizeof(uint32_t));
    __sanitizer_cov_trace_pc_guard_init(p, p + edgeCount);
    return p;
}
–sanitizer-coverage-inline-8bit-counters

开启该选项后,会在每个 Edge 插入一个累加器,每经历过一次,该累加器加一,受 sanitizer-coverage-level 影响。

值得注意的是,该功能存在与 gcc/llvm 实现不一致的地方:不会在 constructor 插入 void __sanitizer_cov_8bit_counters_init(char *start, char *stop),而是在 package 初始化阶段插入函数调用 uint8_t *__cj_sancov_8bit_counters_ctor(uint64_t edgeCount)。

__cj_sancov_pc_guard_ctor 回调函数需要开发者自行实现,开启 SanCov 的 package 会尽可能早地调用该回调函数,入参是该 Package 的 Edge 个数,返回值是通常是 calloc 创建的内存区域。

如果需要调用 __sanitizer_cov_8bit_counters_init,建议在 __cj_sancov_8bit_counters_ctor 中调用,使用动态创建的缓冲区计算该函数的入参和返回值。

一个标准的__cj_sancov_8bit_counters_ctor参考实现如下:

uint8_t *__cj_sancov_8bit_counters_ctor(uint64_t edgeCount) {
    uint8_t *p = (uint8_t *) calloc(edgeCount, sizeof(uint8_t));
    __sanitizer_cov_8bit_counters_init(p, p + edgeCount);
    return p;
}
-sanitizer-coverage-inline-bool-flag

开启该选项后,会在每个 Edge 插入布尔值,经历过的 Edge 对应的布尔值会被设置为 True,受 sanitizer-coverage-level 影响。

值得注意的是,该功能存在与 gcc/llvm 实现不一致的地方:不会在 constructor 插入 void __sanitizer_cov_bool_flag_init(bool *start, bool *stop),而是在 package 初始化阶段插入函数调用 bool *__cj_sancov_bool_flag_ctor(uint64_t edgeCount)。

__cj_sancov_bool_flag_ctor 回调函数需要开发者自行实现,开启 SanCov 的 package 会尽可能早地调用该回调函数,入参是该 Package 的 Edge 个数,返回值是通常是 calloc 创建的内存区域。

如果需要调用 __sanitizer_cov_bool_flag_init,建议在 __cj_sancov_bool_flag_ctor 中调用,使用动态创建的缓冲区计算该函数的入参和返回值。

一个标准的__cj_sancov_8bit_counters_ctor参考实现如下:

bool *__cj_sancov_bool_flag_ctor(uint64_t edgeCount) {
    bool *p = (bool *) calloc(edgeCount, sizeof(bool));
    __sanitizer_cov_bool_flag_init(p, p + edgeCount);
    return p;
}
–sanitizer-coverage-pc-table

该编译选项用于提供插桩点和源码之间的对应关系,当前只提供精确到函数级的对应关系。需要与 --sanitizer-coverage-trace-pc-guard、–sanitizer-coverage-inline-8bit-counters、–sanitizer-coverage-inline-bool-flag 共用,至少需要开启其中一项,可以同时开启多项。

值得注意的是,该功能存在与 gcc/llvm 实现不一致的地方:不会在 constructor 插入 void __sanitizer_cov_pcs_init(const uintptr_t *pcs_beg, const uintptr_t *pcs_end);,而是在 package 初始化阶段插入函数调用 void __cj_sancov_pcs_init(int8_t *packageName, uint64_t n, int8_t **funcNameTable, int8_t **fileNameTable, uint64_t *lineNumberTable),各入参含义如下:

  • int8_t *packageName: 字符串,表示包名(插桩用 c 风格的 int8 数组作为入参来表达字符串,下同)。
  • uint64_t n: 共有 n 个函数被插桩。
  • int8_t **funcNameTable: 长度为 n 的字符串数组,第 i 个插桩点对应的函数名为 funcNameTable[i]。
  • int8_t **fileNameTable: 长度为 n 的字符串数组,第 i 个插桩点对应的文件名为 fileNameTable[i]。
  • uint64_t *lineNumberTable: 长度为 n 的 uint64 数组,第 i 个插桩点对应的行号为 lineNumberTable[i]。

如果需要调用 __sanitizer_cov_pcs_init,需要自行完成仓颉 pc-table 到 C 语言 pc-table 的转化。

–sanitizer-coverage-stack-depth

开启该编译选项后,由于仓颉无法获取 SP 指针的值,只能在每个函数入口处插入调用 __updateSancovStackDepth,在 C 侧实现该函数即可获得 SP 指针。

一个标准的 updateSancovStackDepth 实现如下:

thread_local void* __sancov_lowest_stack;

void __updateSancovStackDepth()
{
    register void* sp = __builtin_frame_address(0);
    if (sp < __sancov_lowest_stack) {
        __sancov_lowest_stack = sp;
    }
}
–sanitizer-coverage-trace-compares

开启该选项后,会在所有的 compare 指令和 match 指令调用前插入函数回调函数,具体列表如下,与 LLVM 系的 API 功能一致。参考 Tracing data flow。

void __sanitizer_cov_trace_cmp1(uint8_t Arg1, uint8_t Arg2);
void __sanitizer_cov_trace_const_cmp1(uint8_t Arg1, uint8_t Arg2);
void __sanitizer_cov_trace_cmp2(uint16_t Arg1, uint16_t Arg2);
void __sanitizer_cov_trace_const_cmp2(uint16_t Arg1, uint16_t Arg2);
void __sanitizer_cov_trace_cmp4(uint32_t Arg1, uint32_t Arg2);
void __sanitizer_cov_trace_const_cmp4(uint32_t Arg1, uint32_t Arg2);
void __sanitizer_cov_trace_cmp8(uint64_t Arg1, uint64_t Arg2);
void __sanitizer_cov_trace_const_cmp8(uint64_t Arg1, uint64_t Arg2);
void __sanitizer_cov_trace_switch(uint64_t Val, uint64_t *Cases);
–sanitizer-coverage-trace-memcmp

该编译选项用于在 String 、 Array 等比较中反馈前缀比较信息。开启该选项后,会对 String 和 Array 的比较函数前插入函数回调函数。具体对于以下对各 String 和 Array 的 API,分别插入对应桩函数:

  • String==: __sanitizer_weak_hook_memcmp
  • String.startsWith: __sanitizer_weak_hook_memcmp
  • String.endsWith: __sanitizer_weak_hook_memcmp
  • String.indexOf: __sanitizer_weak_hook_strstr
  • String.replace: __sanitizer_weak_hook_strstr
  • String.contains: __sanitizer_weak_hook_strstr
  • CString==: __sanitizer_weak_hook_strcmp
  • CString.startswith: __sanitizer_weak_hook_memcmp
  • CString.endswith: __sanitizer_weak_hook_strncmp
  • CString.compare: __sanitizer_weak_hook_strcmp
  • CString.equalsLower: __sanitizer_weak_hook_strcasecmp
  • Array==: __sanitizer_weak_hook_memcmp
  • ArrayList==: __sanitizer_weak_hook_memcmp

实验性功能选项

–experimental [frontend]

启用实验性功能,允许在命令行使用其他实验性功能选项。

注意
使用实验性功能生成的二进制文件有可能会存在潜在的运行时问题,请注意使用该选项的风险。

其他功能

编译器报错信息显示颜色

对于 Windows 版本的仓颉编译器,只有运行于 Windows10 version 1511(Build 10586) 或更高版本的系统,编译器报错信息才显示颜色,否则不显示颜色。

设置 build-id

通过 --link-options "--build-id=<arg>"1 可以透传链接器选项以设置 build-id。

编译 Windows 目标时不支持此功能。

设置 rpath

通过 --link-options "-rpath=<arg>"1 可以透传链接器选项以设置 rpath。

编译 Windows 目标时不支持此功能。

增量编译

通过 --incremental-compile[frontend]开启增量编译。开启后,cjc会在编译时根据前次编译的缓存文件加快此次编译的速度。

cjc 用到的环境变量

这里介绍一些仓颉编译器在编译代码的过程中可能使用到的环境变量。

TMPDIR 或者 TMP

仓颉编译器会将编译过程中产生的临时文件放置到临时目录中。默认情况下 Linux 以及 macOS 操作系统会放在 /tmp 目录下;Windows 操作系统会放在 C:\Windows\Temp 目录下。仓颉编译器也支持自行设置临时文件存放目录,Linux 以及 macOS 操作系统上通过设置环境变量 TMPDIR 来更改临时文件目录,Windows 操作系统上通过设置环境变量 TMP 来更改临时文件目录。

例如:

在 Linux shell 中

export TMPDIR=/home/xxxx

在 Windows cmd 中

set TMP=D:\\xxxx

2.关键字

关键字是不能作为标识符使用的特殊字符串,仓颉语言的关键字如下表所示:

关键字关键字关键字
asabstractbreak
Boolcasecatch
classconstcontinue
Runedoelse
enumextendfor
funcfalsefinally
foreignFloat16Float32
Float64ifin
isinitimport
interfaceInt8Int16
Int32Int64IntNative
letmutmain
macromatchNothing
openoperatoroverride
proppublicpackage
privateprotectedquote
redefreturnspawn
superstaticstruct
synchronizedtrythis
truetypethrow
ThisunsafeUnit
UInt8UInt16UInt32
UInt64UIntNativevar
VArraywherewhile

3.操作符

下表列出了仓颉支持的所有操作符的优先级及结合性,其中优先级一栏数值越小,对应操作符的优先级越高。

操作符优先级含义示例结合方向
@0宏调用@id右结合
.1成员访问expr.id左结合
[]1索引expr[expr]左结合
()1函数调用expr(expr)左结合
++2自增var++
- -2自减var- -
?2问号expr?.id, expr?[expr], expr?(expr), expr?{expr}
!3按位求反、逻辑非!expr右结合
-3一元负号-expr右结合
**4幂运算expr ** expr右结合
*, /5乘法,除法expr * expr, expr / expr左结合
%5取模expr % expr左结合
+, -6加法,减法expr + expr, expr - expr左结合
<<7按位左移expr << expr左结合
>>7按位右移expr >> expr左结合
. .8区间操作符expr. .expr
. .=8含步长的区间操作符expr. .=expr
<9小于expr < expr
<=9小于等于expr <= expr
>9大于expr > expr
>=9大于等于expr >= expr
is9类型检查expr is Type
as9类型转换expr as Type
==10判等expr == expr
!=10判不等expr != expr
&11按位与expr & expr左结合
^12按位异或expr ^ expr左结合
13按位或expr 丨 expr左结合
&&14逻辑与expr && expr左结合
丨丨15逻辑或expr 丨丨 expr左结合
??16coalescing 操作符expr ?? expr右结合
丨>17pipeline 操作符id 丨> expr左结合
~>17composition 操作符expr ~> expr左结合
=18赋值id = expr
**=18复合运算符id **= expr
*=18复合运算符id *= expr
/=18复合运算符id /= expr
%=18复合运算符id %= expr
+=18复合运算符id += expr
-=18复合运算符id -= expr
<<=18复合运算符id <<= expr
>>=18复合运算符id >>= expr
&=18复合运算符id &= expr
^=18复合运算符id ^= expr
丨=18复合运算符id 丨= expr
&&=18复合运算符id &&= expr
丨 丨=18复合运算符id 丨 丨= expr

4.操作符函数

下表列出了仓颉支持的所有操作符函数。

操作符函数函数签名示例
[] (索引取值)operator func [](index1: T1, index2: T2, …): Rthis[index1, index2, …]
[] (索引赋值)operator func [](index1: T1, index2: T2, …, value!: TN): Rthis[index1, index2, …] = value
()operator func ()(param1: T1, param2: T2, …): Rthis(param1, param2, …)
!operator func !(): R!this
**operator func **(other: T): Rthis ** other
*operator func *(other: T): Rthis * other
/operator func /(other: T): Rthis / other
%operator func %(other: T): Rthis % other
+operator func +(other: T): Rthis + other
-operator func -(other: T): Rthis - other
<<operator func <<(other: T): Rthis << other
>>operator func >>(other: T): Rthis >> other
<operator func <(other: T): Rthis < other
<=operator func <=(other: T): Rthis <= other
>operator func >(other: T): Rthis > other
>=operator func >=(other: T): Rthis >= other
==operator func ==(other: T): Rthis == other
!=operator func !=(other: T): Rthis != other
&operator func &(other: T): Rthis & other
^operator func ^(other: T): Rthis ^ other
operator func 丨(other: T): Rthis 丨 other

5.TokenKind 类型

public enum TokenKind <: ToString {
    DOT|                      /*  "."           */
    COMMA|                    /*  ","           */
    LPAREN|                   /*  "("           */
    RPAREN|                   /*  ")"           */
    LSQUARE|                  /*  "["           */
    RSQUARE|                  /*  "]"           */
    LCURL|                    /*  "{"           */
    RCURL|                    /*  "}"           */
    EXP|                      /*  "**"          */
    MUL|                      /*  "*"           */
    MOD|                      /*  "%"           */
    DIV|                      /*  "/"           */
    ADD|                      /*  "+"           */
    SUB|                      /*  "-"           */
    INCR|                     /*  "++"          */
    DECR|                     /*  "--"          */
    AND|                      /*  "&&"          */
    OR|                       /*  "||"          */
    COALESCING|               /*  "??"          */
    PIPELINE|                 /*  "|>"          */
    COMPOSITION|              /*  "~>"          */
    NOT|                      /*  "!"           */
    BITAND|                   /*  "&"           */
    BITOR|                    /*  "|"           */
    BITXOR|                   /*  "^"           */
    BITNOT|                   /*  "~"           */
    LSHIFT|                   /*  "<<"          */
    RSHIFT|                   /*  ">>"          */
    COLON|                    /*  ":"           */
    SEMI|                     /*  ";"           */
    ASSIGN|                   /*  "="           */
    ADD_ASSIGN|               /*  "+="          */
    SUB_ASSIGN|               /*  "-="          */
    MUL_ASSIGN|               /*  "*="          */
    EXP_ASSIGN|               /*  "**="         */
    DIV_ASSIGN|               /*  "/="          */
    MOD_ASSIGN|               /*  "%="          */
    AND_ASSIGN|               /*  "&&="         */
    OR_ASSIGN|                /*  "||="         */
    BITAND_ASSIGN|            /*  "&="          */
    BITOR_ASSIGN|             /*  "|="          */
    BITXOR_ASSIGN|            /*  "^="          */
    LSHIFT_ASSIGN|            /*  "<<="         */
    RSHIFT_ASSIGN|            /*  ">>="         */
    ARROW|                    /*  "->"          */
    BACKARROW|                /*  "<-"          */
    DOUBLE_ARROW|             /*  "=>"          */
    RANGEOP|                  /*  ".."          */
    CLOSEDRANGEOP|            /*  "..="         */
    ELLIPSIS|                 /*  "..."         */
    HASH|                     /*  "#"           */
    AT|                       /*  "@"           */
    QUEST|                    /*  "?"           */
    LT|                       /*  "<"           */
    GT|                       /*  ">"           */
    LE|                       /*  "<="          */
    GE|                       /*  ">="          */
    IS|                       /*  "is"          */
    AS|                       /*  "as"          */
    NOTEQ|                    /*  "!="          */
    EQUAL|                    /*  "=="          */
    WILDCARD|                 /*  "_"           */
    INT8|                     /*  "Int8"        */
    INT16|                    /*  "Int16"       */
    INT32|                    /*  "Int32"       */
    INT64|                    /*  "Int64"       */
    INTNATIVE|                /*  "IntNative"   */
    UINT8|                    /*  "UInt8"       */
    UINT16|                   /*  "UInt16"      */
    UINT32|                   /*  "UInt32"      */
    UINT64|                   /*  "UInt64"      */
    UINTNATIVE|               /*  "UIntNative"  */
    FLOAT16|                  /*  "Float16"     */
    FLOAT32|                  /*  "Float32"     */
    FLOAT64|                  /*  "Float64"     */
    RUNE|                     /*  "Rune"        */
    BOOLEAN|                  /*  "Bool"        */
    NOTHING|                  /*  "Nothing"     */
    UNIT|                     /*  "Unit"        */
    STRUCT|                   /*  "struct"      */
    ENUM|                     /*  "enum"        */
    CFUNC|                    /*  "CFunc"       */
    VARRAY|                   /*  "VArray"      */
    THISTYPE|                 /*  "This"        */
    PACKAGE|                  /*  "package"     */
    IMPORT|                   /*  "import"      */
    CLASS|                    /*  "class"       */
    INTERFACE|                /*  "interface"   */
    FUNC|                     /*  "func"        */
    MACRO|                    /*  "macro"       */
    QUOTE|                    /*  "quote"       */
    DOLLAR|                   /*  "$"           */
    LET|                      /*  "let"         */
    VAR|                      /*  "var"         */
    CONST|                    /*  "const"       */
    TYPE|                     /*  "type"        */
    INIT|                     /*  "init"        */
    THIS|                     /*  "this"        */
    SUPER|                    /*  "super"       */
    IF|                       /*  "if"          */
    ELSE|                     /*  "else"        */
    CASE|                     /*  "case"        */
    TRY|                      /*  "try"         */
    CATCH|                    /*  "catch"       */
    FINALLY|                  /*  "finally"     */
    FOR|                      /*  "for"         */
    DO|                       /*  "do"          */
    WHILE|                    /*  "while"       */
    THROW|                    /*  "throw"       */
    RETURN|                   /*  "return"      */
    CONTINUE|                 /*  "continue"    */
    BREAK|                    /*  "break"       */
    IN|                       /*  "in"          */
    NOT_IN|                   /*  "!in"         */
    MATCH|                    /*  "match"       */
    FROM|                     /*  "from"        */
    WHERE|                    /*  "where"       */
    EXTEND|                   /*  "extend"      */
    WITH|                     /*  "with"        */
    PROP|                     /*  "prop"        */
    STATIC|                   /*  "static"      */
    PUBLIC|                   /*  "public"      */
    PRIVATE|                  /*  "private"     */
    PROTECTED|                /*  "protected"   */
    OVERRIDE|                 /*  "override"    */
    REDEF|                    /*  "redef"       */
    ABSTRACT|                 /*  "abstract"    */
    SEALED|                   /*  "sealed"      */
    OPEN|                     /*  "open"        */
    FOREIGN|                  /*  "foreign"     */
    INOUT|                    /*  "inout"       */
    MUT|                      /*  "mut"         */
    UNSAFE|                   /*  "unsafe"      */
    OPERATOR|                 /*  "operator"    */
    SPAWN|                    /*  "spawn"       */
    SYNCHRONIZED|             /*  "synchronized */
    UPPERBOUND|               /*  "<:"          */
    MAIN|                     /*  "main"        */
    IDENTIFIER|               /*  "x"           */
    PACKAGE_IDENTIFIER|       /*  "x-y"         */
    INTEGER_LITERAL|          /*  e.g. "1"      */
    RUNE_BYTE_LITERAL|        /*  e.g. "b'x'"   */
    FLOAT_LITERAL|            /*  e.g. "'1.0'"  */
    COMMENT|                  /*  e.g. "//xx"     */
    NL|                       /*  newline         */
    END|                      /*  end of file     */
    SENTINEL|                 /*  ";"             */
    RUNE_LITERAL|             /*  e.g. "r'x'"      */
    STRING_LITERAL|           /*  e.g. ""xx""     */
    JSTRING_LITERAL|          /*  e.g. "J"xx""     */
    MULTILINE_STRING|         /*  e.g. """"aaa""""   */
    MULTILINE_RAW_STRING|     /*  e.g. "#"aaa"#"     */
    BOOL_LITERAL|             /*  "true" or "false"  */
    UNIT_LITERAL|             /*  "()"               */
    DOLLAR_IDENTIFIER|        /*  e.g. "$x"          */
    ANNOTATION|               /*  e.g. "@When"       */
    ILLEGAL
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值