使用 --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.add(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 --import-path ./target

注意:

如果使用仓颉的 CJPM 包管理工具进行编译,可在配置文件 cjpm.toml 中添加 --debug-macro 的编译选项来使用宏的 debug 模式。

compile-option = "--debug-macro"

在 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 模式下,报错信息中会打印出完整的宏展开代码。仓颉宏的 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 模式。

不支持宏调用在宏定义内的调试,会编译报错。

```js
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
}

宏包定义和导入

仓颉语言中宏的定义需要放在由 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
}

需要特殊说明的是,在宏包中,允许其他宏包和非宏包的声明被重导出。在非宏包中仅允许非宏包的声明被重导出。

参考如下示例:

在宏包 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’ 的错误信息。

内置编译标记

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

源码位置

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

@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 函数满足以下两点要求:

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

函数内部不能调用仓颉方法。

@Frozen

@Frozen 标记可用于修饰函数和属性。如果确定某个函数、属性在将来的版本更新中不会去修改它的内部实现,那么可以使用 @Frozen 对其进行标记,该标记代表开发者对该函数/属性在未来版本演进的一种承诺。被 @Frozen 修饰的函数和属性,在后续的升级版本中,签名和函数体都不能发生任何变化。这意味着,前后两个代码版本,在相同编译器、相同编译选项的情况下,该函数或属性的生成产物完全一致。

@Frozen 标记可被用于修饰:

全局函数

类、结构、接口、扩展、枚举中的函数
类、接口、扩展中的属性

@Frozen 标记不可被用于修饰:

除了函数和属性以外的其他类型声明
嵌套函数
表达式
使用示例如下:

@Frozen
public func test(): Unit {}

public class testClass {
    @Frozen
    public func testFunc(): Unit {}

    @Frozen
    public prop testProp: Unit {
        get() {}
    }
}

@Attribute

仓颉语言内部提供 @Attribute 标记,开发者通过内置的 @Attribute 来对某个声明设置属性值,从而达到标记声明的目的。属性值可以是 identifier 或者 string,下面是一个简单的例子,这段示例代码为变量 cnt 添加了一个 identifier 类型的属性 State,为变量 bcnt 添加了一个 string 类型的属性 “Binding”。

@Attribute[State] var cnt = 0       // identifier
@Attribute["Binding"] var bcnt = 0  // string

同时,标准库 std.ast 包提供了 getAttrs() 方法用于获取节点的属性,以及 hasAttr(attrs: String) 方法用于判断当前节点是否具有某个属性,下面是一个具体的例子。

宏定义如下:

public macro Component(input: Tokens): Tokens {
    var varDecl = parseDecl(input)
    if (varDecl.hasAttr("State")) { // 如果该节点被标记了属性且值为 “State” 返回 true, 否则返回 false
        var attrs = varDecl.getAttrs() // 返回一组 Tokens
        println(attrs[0].value)
    }
    return input
}

宏调用如下:

@Component(
    @Attribute[State] var cnt = 0
)

@Deprecated

@Deprecated 表示此 API 已废弃,虽然暂时可用,但未来将被移除或更改,建议其他开发者不要调用此 API。例如:

@Deprecated["用boo代替", since: "1.3.4"]
func foo() {}

main() {
    foo()
}

编译器编译时将提供告警信息:

内置编译标记

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

源码位置

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

  • @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 函数满足以下两点要求:

  • 函数的整体执行时间不宜太长。例如:不允许函数内部存在很大的循环;不允许函数内部产生阻塞行为,例如:调用 sleep、wait 等函数。
  • 函数内部不能调用仓颉方法。
    @Frozen
    @Frozen 标记可用于修饰函数和属性。如果确定某个函数、属性在将来的版本更新中不会去修改它的内部实现,那么可以使用 @Frozen 对其进行标记,该标记代表开发者对该函数/属性在未来版本演进的一种承诺。被 @Frozen 修饰的函数和属性,在后续的升级版本中,签名和函数体都不能发生任何变化。这意味着,前后两个代码版本,在相同编译器、相同编译选项的情况下,该函数或属性的生成产物完全一致。

@Frozen 标记可被用于修饰:

全局函数
类、结构、接口、扩展、枚举中的函数
类、接口、扩展中的属性
@Frozen 标记不可被用于修饰:

除了函数和属性以外的其他类型声明
嵌套函数
表达式
使用示例如下:

@Frozen
public func test(): Unit {}

public class testClass {
    @Frozen
    public func testFunc(): Unit {}

    @Frozen
    public prop testProp: Unit {
        get() {}
    }
}

@Attribute
仓颉语言内部提供 @Attribute 标记,开发者通过内置的 @Attribute 来对某个声明设置属性值,从而达到标记声明的目的。属性值可以是 identifier 或者 string,下面是一个简单的例子,这段示例代码为变量 cnt 添加了一个 identifier 类型的属性 State,为变量 bcnt 添加了一个 string 类型的属性 “Binding”。

@Attribute[State] var cnt = 0       // identifier
@Attribute["Binding"] var bcnt = 0  // string

同时,标准库 std.ast 包提供了 getAttrs() 方法用于获取节点的属性,以及 hasAttr(attrs: String) 方法用于判断当前节点是否具有某个属性,下面是一个具体的例子。

宏定义如下:

public macro Component(input: Tokens): Tokens {
    var varDecl = parseDecl(input)
    if (varDecl.hasAttr("State")) { // 如果该节点被标记了属性且值为 “State” 返回 true, 否则返回 false
        var attrs = varDecl.getAttrs() // 返回一组 Tokens
        println(attrs[0].value)
    }
    return input
}

宏调用如下:

@Component(
    @Attribute[State] var cnt = 0
)
@Deprecated
@Deprecated 表示此 API 已废弃,虽然暂时可用,但未来将被移除或更改,建议其他开发者不要调用此 API。例如:

@Deprecated["用boo代替", since: "1.3.4"]
func foo() {}

main() {
    foo()
}

编译器编译时将提供告警信息:

warning: function 'foo' is deprecated since 1.3.4. 用boo代替
 ==> file.cj:5:5:
  |
5 |     foo()
  |     ^^^ deprecated
  |
  # note: this warning can be suppressed by setting the compiler option `-Woff deprecated`

1 warning generated, 1 warning printed.

@Deprecated 自定义宏可以应用于以下声明:

类、接口、结构体、枚举、枚举构造器
顶级(全局)函数或变量
静态或非静态的成员函数、成员变量、属性、属性设置器
运算符函数

扩展的成员函数、静态函数、属性或属性设置器
foreign 函数或声明在 foreign 块内的函数
构造函数和主构造函数
抽象的函数和属性
类型别名(包括关联类型)
函数具有默认参数的命名参数
const 变量和函数
宏定义
注解类

@Deprecated 参数

message: String - 描述声明为何废弃、如何迁移等。
since!: ?String - 废弃版本。
strict!: Bool - 默认值为 false,在被该标记修饰的 API 的调用处会触发警告。如果设置为 true,则会触发编译错误。

@Deprecated["Use Macro2", since: "1990", strict: true]
public macro Macro(input: Tokens): Tokens {
    return input
}
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐