1. 布尔值背后的那棵树

一条表达式里同时出现与和或,靠眼睛扫过去很容易读错。尤其是权限条件,代码短短一行,意思却可能差一整个范围:管理员可以操作,或者已付费并且在线;管理员或已付费的用户,都必须在线。要确认差别,不能只盯最后那个布尔值。

本例准备了两个只差括号的位置的输入。第一条是一个真值,或上两个假值的与;第二条把前面的真值和假值先括起来,再与后面的假值结合。它们结果不同。页面同时给出解析后的树和词元细节,让这个变化有地方可查。

这里选用 better-parse,并把其 Kotlin 公共源码接到鸿蒙原生环境。应用里没有重新实现一份字符扫描器,词元偏移、空白忽略、组合规则以及整段解析都由原库提供。示例补的部分是这门布尔语法,以及三个变量的取值环境。

2. 一层括号改变了什么

表达式默认先处理与,再处理或。因此第一条输入会把两个假值组成一个与节点,然后把这个节点与左侧真值组成或节点,结果为真。第二条输入先在括号里得到真,再和最后一个假值做与,结果就变成假。

这一点在语法树里能直接看到:根节点从或变成了与。它比一段解释文字更容易对照,也比仅显示结果更容易发现自己漏掉了括号。Kotlin 返回节点的结构文本,界面根据这段文本添加换行、缩进和分支标记,原始结构也保留在下面方便核对。页面只负责树的排版,词法和语法组合仍然调用原库接口。

页面另有一个权限条件样例,判断在线并且不是管理员。固定环境里,在线为真、管理员为假、已付费为真。变量名区分大小写,不存在的变量会报错,没有把查不到的名字都当作假。否则错拼一个名称时,程序看起来也能正常返回,排查会非常费劲。

所有变量名先在完整语法树中收集,再检查是否在环境里。这个顺序很有必要:逻辑与和逻辑或求值时可以短路,一条实际上没有执行到的分支也可能藏着拼错的变量。先做名称校验,便不会因为另一边已经确定结果而漏掉错误。

3. 从词元详情找回那几个字符

页面采用深色编辑器,输入区下面摆出三个变量的当前状态。左边的结构区一直保留语法树,右边默认显示词元数量,切到词元详情后再逐条展开。每个非空白词元都有类型、原始文本、字符偏移,以及从一开始计数的行列位置。比如逻辑非是一条词元,括号各有自己的位置,变量名称则保留输入文本。空白参与扫描,在结果展示里过滤掉。

偏移和行列回答的是两种问题。偏移适合定位输入字符串中的位置;行列更适合多行编辑器的提示。当前页面只是短文本输入,所以两者同时展示用来观察规则,还没有实现点击词元就把光标移到相应位置的编辑体验。

词元数量也可以帮助核对界面有没有漏掉内容。一条带逻辑非的权限条件里,变量、与符号、逻辑非和另一个变量都是独立的非空白词元;添加一组括号后,数量随之增加。显示模式切换只决定是否展开这些行,不重新解释另一套语法,因此树视图和词元视图应该指向相同输入。看到数量意外减少时,先检查词元是否被设成忽略,再检查是否只消费了一个前缀。

空白处理则走相反的方向。把条件拆成两行,或者在符号左右加几段空格,词元中的原始偏移和行列会变化,但最终语法树与求值应保持一致。这种小改动很适合检查扫描和解析职责是否分明:输入字符变了,语义没变;括号变了,结构才应该跟着变。

正则词元必须从当前位置开始匹配。这句话看起来平常,实际上决定了非法字符会不会被跳过去。输入前面若有一个问号,而后面有合法名称,解析器应该在问号处停下来,而不是在后半段找到了名称就假装整个输入都有效。

better-parse 的上游原生实现会给正则增加输入起点约束,并针对当前偏移创建相对输入。此次鸿蒙接入复用的就是这份实现,没有把匹配改为在全字符串任意搜索。设备检查里专门比较从非法字符处开始和从合法名称处开始这两种结果,用来检验平台实现是否保留了这条规则。

4. 两层组合写出了优先级

代码里没有一个总括所有情况的巨大判断。词元先定义,原子项和括号组成基本项;与规则在基本项上组合,或规则再在与规则上组合。阅读这两行组合代码,就能看出默认优先级在哪里确定。

下面是 harmony/src/Bridge.kt 的完整内容。词元声明、布尔节点、递归文法、固定变量、结构输出与错误检查在同一份文件中。从 id、not、term 到 BoolExpr 都有完整声明,conjunction 和 rootParser 的组合可以沿着这些定义往回查看;evaluate 与 runChecks 则分别供页面交互和设备检查调用。

package harmony
import com.github.h0tk3y.betterParse.combinators.*
import com.github.h0tk3y.betterParse.grammar.*
import com.github.h0tk3y.betterParse.lexer.*
import com.github.h0tk3y.betterParse.parser.Parser

private sealed class BoolExpr {
    data class Literal(val value:Boolean):BoolExpr()
    data class Variable(val name:String):BoolExpr()
    data class Not(val body:BoolExpr):BoolExpr()
    data class And(val left:BoolExpr,val right:BoolExpr):BoolExpr()
    data class Or(val left:BoolExpr,val right:BoolExpr):BoolExpr()
}
private object BooleanGrammar:Grammar<BoolExpr>() {
    val id by regexToken("[a-zA-Z][a-zA-Z0-9_]*")
    val left by literalToken("(")
    val right by literalToken(")")
    val not by literalToken("!")
    val and by literalToken("&")
    val orToken by literalToken("|")
    val whitespace by regexToken("\\s+",ignore=true)
    val atom:Parser<BoolExpr> by id map { when(it.text) {"true"->BoolExpr.Literal(true);"false"->BoolExpr.Literal(false);else->BoolExpr.Variable(it.text)} }
    val negation:Parser<BoolExpr> by -not * parser(::term) map { BoolExpr.Not(it) }
    val nested by -left * parser(::rootParser) * -right
    val term:Parser<BoolExpr> by atom or negation or nested
    val conjunction by leftAssociative(term,and) { a,_,b -> BoolExpr.And(a,b) }
    override val rootParser:Parser<BoolExpr> by leftAssociative(conjunction,orToken) {a,_,b->BoolExpr.Or(a,b)}
}
private val variables=mapOf("online" to true,"admin" to false,"paid" to true)
private fun BoolExpr.resolve():Boolean=when(this) {
    is BoolExpr.Literal->value
    is BoolExpr.Variable->variables[name]?:throw IllegalArgumentException("未知变量:$name")
    is BoolExpr.Not->!body.resolve()
    is BoolExpr.And->left.resolve() && right.resolve()
    is BoolExpr.Or->left.resolve() || right.resolve()
}
private fun BoolExpr.names():Set<String> = when(this) {is BoolExpr.Variable->setOf(name);is BoolExpr.Literal->emptySet();is BoolExpr.Not->body.names();is BoolExpr.And->left.names()+right.names();is BoolExpr.Or->left.names()+right.names()}
private fun BoolExpr.ast():String=when(this) {is BoolExpr.Literal->value.toString();is BoolExpr.Variable->"VAR($name)";is BoolExpr.Not->"NOT(${body.ast()})";is BoolExpr.And->"AND(${left.ast()}, ${right.ast()})";is BoolExpr.Or->"OR(${left.ast()}, ${right.ast()})"}
private fun parse(s:String):BoolExpr {
    require(s.length<=512 && s.count{it=='('}<=24) {"表达式过长或嵌套过深"}
    val expr=BooleanGrammar.parseToEnd(s)
    require(expr.names().all{it in variables}) {"未知变量:${expr.names().filter{it !in variables}.joinToString()}"}
    return expr
}
fun evaluate(input:String,mode:Int):String=try {
    val expr=parse(input)
    val tokens=BooleanGrammar.tokenizer.tokenize(input).filter{!it.type.ignored}.toList()
    val rows=mutableListOf(row("计算结果",expr.resolve().toString(),if(expr.resolve())1.0 else 0.0),row("变量环境","online=true · admin=false · paid=true"),row("抽象语法树",expr.ast()))
    if(mode==1) tokens.forEachIndexed { i,t->rows+=row("词元 ${i+1} · ${t.type.name}","${t.text} · offset=${t.offset} · ${t.row}:${t.column}") }
    else rows+=row("非空白词元数",tokens.size.toString(),tokens.size.toDouble())
    result("布尔解析完成 · ${tokens.size} 个词元",rows)
} catch(e:Exception) {result("表达式解析失败",listOf(row("错误反馈",e.message?:e.toString()),row("修正方法","检查括号、运算符和变量名,再次执行")),false)}
private fun value(s:String)=parse(s).resolve()
private fun rejects(s:String)=try{parse(s);false}catch(_:Exception){true}
fun runChecks():String=checks(listOf(
    "字面真值" to {value("true")},
    "字面假值" to {!value("false")},
    "变量取值" to {value("online") && !value("admin")},
    "逻辑非" to {value("!admin")},
    "重复逻辑非" to {value("!!paid")},
    "与运算" to {!value("online & admin")},
    "或运算" to {value("admin | paid")},
    "与优先于或" to {value("true | false & false")},
    "括号改变顺序" to {!value("(true | false) & false")},
    "空白词元忽略" to {value("\t online\n & !admin ")},
    "匹配必须从当前位置开始" to {regexToken("[a-z]+").match("?abc",0)==0 && regexToken("[a-z]+").match("?abc",1)==3},
    "拒绝缺操作数" to {rejects("online &")},
    "拒绝尾随词元" to {rejects("online paid")},
    "拒绝未知变量" to {rejects("unknown | online")},
    "拒绝非法字符" to {rejects("online @ paid")},
    "错误后恢复" to {rejects("(") && value("online & !admin")}
))

private fun quote(s: String): String = "\"" + buildString {
    s.forEach { c -> when(c) { '"' -> append("\\\""); '\\' -> append("\\\\"); '\n' -> append("\\n"); '\r' -> append("\\r"); '\t' -> append("\\t"); else -> if(c.code < 32) append("\\u"+c.code.toString(16).padStart(4,'0')) else append(c) } }
} + "\""
private fun row(label: String, value: String, metric: Double = 0.0): String = "{\"label\":"+quote(label)+",\"value\":"+quote(value)+",\"metric\":"+(if(metric.isFinite()) metric else 0.0)+"}"
private fun result(summary: String, rows: List<String>, ok: Boolean = true): String = "{\"ok\":$ok,\"summary\":"+quote(summary)+",\"rows\":"+rows.joinToString(",","[","]")+"}"
private fun checks(cases: List<Pair<String, () -> Boolean>>): String {
    var passed = 0
    val details = cases.joinToString("\n") { (name,test) ->
        try { if(test()) { passed++; "PASS $name" } else "FAIL $name" }
        catch(e: Throwable) { "FAIL $name: ${e.message}" }
    }
    return "{\"passed\":$passed,\"total\":${cases.size},\"detail\":"+quote(details)+"}"
}

名称词元同时接收布尔字面量和普通变量,再在映射时区分。这可以让一个包含真值前缀的名称仍按完整名称读取,不至于先把前几个字符吃成真值,剩下半个名称无处安放。示例只支持小写字面量,不把这个约定扩展成整个语言都忽略大小写。

逻辑非通过延迟引用再次解析基本项,因此连续两个逻辑非也可以工作。括号通过延迟引用访问根规则,避免语法对象初始化时过早取得还没有赋值的解析器。解析入口要求整段消费,输入两个没有运算符连接的变量会得到错误,不能只拿第一个变量的结果结束。

5. 在设备上看树,再制造一次失败

运行环境为 API 24 的华为电脑真机(电脑模式、ARM64)与 x86_64 模拟器。两端安装本次编译的签名应用,完成页面操作和 16 项原库检查,均返回 16/16。下面保留连续操作过程。

演示先从权限条件进入,切到词元详情,随后比较默认优先级和增加括号两个样例。关注语法树根节点和布尔结果一起变化,才能区分是解析规则变化,还是界面只替换了显示文本。

错误样例把与运算符后面的操作数删掉。原先的结构树区域会换成完整错误说明,输入区仍保留这条表达式。再点回完整的权限条件,页面应当重新返回正常结构与结果。也可以自行改好文本,点击编辑器右侧的拆解按钮。非法字符、未知变量和多余尾部各有设备内检查,覆盖几种不同来源的失败。

十六项检查包括字面真值、假值、变量、重复取反、与或、默认优先级、括号、空白、当前位置匹配、缺操作数、尾随词元、未知变量、非法字符和失败后恢复。它们在 Kotlin 侧直接调用同一个语法对象与实际正则实现,页面只负责展示逐项结果。

6. 平台层只补两处实际声明

构建环境按照 CPF-KMP-CMP 官方快速开始 配置,使用 OHOS Kotlin/Native 2.2.21-1.1.0。公共源集保留在原目录,鸿蒙适配独立放入平台目录,示例页面置于原仓库示例目录下的鸿蒙子目录。

这次需要补齐的不是布尔运算,而是公共代码里的两个平台声明。正则词元类需要对应的实际实现;标注正则文本的语言注解也需要处理,不能直接带入 JVM 上使用的注解依赖。鸿蒙目录中提供了不依赖 JVM 注解包的实际注解,并复用上游原生正则词元实现。

第一次编译时,新增实际注解的类和构造函数已经带有平台关键字,但三个属性还没有标为实际声明,编译器会指出对应关系不完整。修正的是新增文件中的属性声明,公共代码无需改动。这个错误也说明仅把同名类放进平台目录并不够,声明签名和属性要完整对应。

构建配置分别生成 ARM64 与 X64 产物。它们使用同一批公共源码及同一份示例语法,不在不同设备上走两套规则。原有公共源码中的内联和旧版本抑制项会产生编译警告,平台产物能生成不代表日志中完全没有警告,这两件事要分别看。

桥接出口只接收表达式和显示模式,把结果、语法树与词元行组合成结构化返回。页面不需要知道 Kotlin 节点类的内存布局,也不持有节点地址;错误通过相同出口返回,保持一次点击只产生一份明确的反馈。

把接口定义和应用配置放到一起看,库到页面的调用次序才完整。下表中 entry 路径均位于 example/harmony 中;生成的 C 头文件由原生构建脚本输出,不需要手工猜测导出结构。

文件

在调用链中的职责

harmony/src/Bridge.kt

原库调用、返回编码与设备检查

harmony/native-config.json

共享源集、双 ABI 依赖和编译选项

harmony/build-native.py

编译 KLIB、链接动态库并生成匹配头文件

entry/src/main/cpp/napi_init.cpp

参数检查、原生调用与返回值生命周期

entry/src/main/cpp/CMakeLists.txt

按 ABI 选择动态库和头文件

entry/src/main/cpp/types/libentry/index.d.ts

ArkTS 能看到的接口类型

entry/src/main/ets/pages/Index.ets

完整输入、运行反馈与独立界面

6.1 两份 Native actual 的完整实现

Language 注解只补平台声明,RegexToken 使用上游 Native 实现并保留当前位置约束。它们与公共源集一起编译。两份文件的包名、actual 属性和正则匹配规则都要保留:

package com.github.h0tk3y.betterParse.lexer
public actual annotation class Language actual constructor(actual val value:String,actual val prefix:String,actual val suffix:String)
package com.github.h0tk3y.betterParse.lexer

public actual class RegexToken : Token {
    private val pattern: String
    private val regex: Regex

    private companion object {
        const val inputStartPrefix = "\\A"
    }

    private fun prependPatternWithInputStart(patternString: String, options: Set<RegexOption>) =
        if (patternString.startsWith(inputStartPrefix))
            patternString.toRegex(options)
        else {
            ("$inputStartPrefix(?:$patternString)").toRegex(options)
        }

    public actual constructor(name: String?, @Language("RegExp", "", "") patternString: String, ignored: Boolean)
            : super(name, ignored) {
        pattern = patternString
        regex = prependPatternWithInputStart(patternString, emptySet())
    }

    public actual constructor(name: String?, regex: Regex, ignored: Boolean)
            : super(name, ignored) {
        pattern = regex.pattern
        this.regex = prependPatternWithInputStart(pattern, emptySet())
    }

    private class RelativeInput(val fromIndex: Int, val input: CharSequence) : CharSequence {
        override val length: Int get() = input.length - fromIndex
        override fun get(index: Int): Char = input[index + fromIndex]
        override fun subSequence(startIndex: Int, endIndex: Int) =
            input.subSequence(startIndex + fromIndex, endIndex + fromIndex)

        override fun toString(): String = error("unsupported operation")
    }

    override fun match(input: CharSequence, fromIndex: Int): Int {
        val relativeInput = RelativeInput(fromIndex, input)

        return regex.find(relativeInput)?.range?.let {
            val length = it.last - it.first + 1
            length
        } ?: 0
    }

    override fun toString(): String = "${name ?: ""} [$pattern]" + if (ignored) " [ignorable]" else ""
}

6.2 词元与结构从哪里返回

N-API 先核对两个参数,再按 UTF-8 字节数分配缓冲区。Kotlin 返回的字符串先复制到 ArkTS 值中,随后调用 DisposeString 释放。下面给出整个文件,包含模块注册;只有 Evaluate 函数而没有 Init 和 Register,页面就无法从 libentry.so 取得接口。

完整文件:example/harmony/entry/src/main/cpp/napi_init.cpp。

#include "napi/native_api.h"
#include "betterparse_api.h"
#include <string>
static napi_value Copy(napi_env env,const char* text) {
  napi_value value;
  napi_create_string_utf8(env,text?text:"{}",NAPI_AUTO_LENGTH,&value);
  if(text) betterparse_symbols()->DisposeString(text);
  return value;
}
static napi_value Evaluate(napi_env env,napi_callback_info info) {
  size_t count=2;napi_value args[2];napi_get_cb_info(env,info,&count,args,nullptr,nullptr);
  size_t length=0;int32_t mode=0;
  if(count!=2 || napi_get_value_string_utf8(env,args[0],nullptr,0,&length)!=napi_ok || napi_get_value_int32(env,args[1],&mode)!=napi_ok) {
    napi_throw_type_error(env,nullptr,"evaluate requires text and a mode");return nullptr;
  }
  if(length>16384){napi_throw_range_error(env,nullptr,"Input exceeds demo limit");return nullptr;}
  std::string text(length+1,'\0');napi_get_value_string_utf8(env,args[0],text.data(),text.size(),&length);text.resize(length);
  return Copy(env,betterparse_symbols()->kotlin.root.harmony.evaluate(text.c_str(),mode));
}
static napi_value Check(napi_env env,napi_callback_info info){return Copy(env,betterparse_symbols()->kotlin.root.harmony.runChecks());}
static napi_value Init(napi_env env,napi_value exports){
  napi_property_descriptor properties[]={
    {"evaluate",nullptr,Evaluate,nullptr,nullptr,nullptr,napi_default,nullptr},
    {"runChecks",nullptr,Check,nullptr,nullptr,nullptr,napi_default,nullptr}};
  napi_define_properties(env,exports,2,properties);return exports;
}
static napi_module module={1,0,nullptr,Init,"entry",nullptr,{0}};
extern "C" __attribute__((constructor)) void Register(){napi_module_register(&module);}

6.3 树形展示接在哪一层

Kotlin 返回结构文本后,页面负责换行和缩进,并保留原始结构供核对。变量状态、词元明细和错误反馈使用同一份结果,不能各自解析一次输入得到不同结论。完整文件展示了输入、树、词元和检查状态之间的更新顺序。

先给出 entry/src/main/cpp/types/libentry/index.d.ts,紧接着是完整的 entry/src/main/ets/pages/Index.ets。类型声明与下面的实际调用保持一致。

export const evaluate: (input: string, mode: number) => string;
export const runChecks: () => string;
import native from 'libentry.so';

interface ResultRow { label: string; value: string; metric: number; }
interface Result { ok: boolean; summary: string; rows: ResultRow[]; }
interface Checks { passed: number; total: number; detail: string; }

@Entry
@Component
struct Index {
  @State input: string = 'online & !admin';
  @State mode: number = 0;
  @State view: Result = { ok: true, summary: '', rows: [] };
  @State checkStatus: string = '16 项规则检查,尚未运行';
  @State details: string = '';
  private samples: string[] = ['online & !admin', 'true | false & false', '(true | false) & false', 'online &'];
  private labels: string[] = ['权限条件', '默认优先级', '加上括号', '错误输入'];

  aboutToAppear(): void { this.refresh(); }
  private refresh(): void {
    try { this.view = JSON.parse(native.evaluate(this.input, this.mode)) as Result; }
    catch (e) { this.view = { ok: false, summary: '表达式调用失败', rows: [{ label: '反馈', value: '修正输入后可再次拆解', metric: 0 }] }; }
  }
  private value(index: number): string { return this.view.rows.length > index ? this.view.rows[index].value : ''; }
  private printedTree(): string {
    // Only format the AST text returned by Kotlin; no expression evaluation here.
    const s: string = this.value(2);
    let indent: number = 0;
    let output: string = '';
    for (let i: number = 0; i < s.length; i++) {
      const c: string = s.charAt(i);
      if (c === '(') { indent++; output += '\n' + '  '.repeat(indent) + '└ '; }
      else if (c === ')') { indent = Math.max(0, indent - 1); }
      else if (c === ',') { output += '\n' + '  '.repeat(indent) + '└ '; }
      else if (c !== ' ') { output += c; }
    }
    return output;
  }
  private check(): void {
    try { const r = JSON.parse(native.runChecks()) as Checks; this.checkStatus = `设备检查 ${r.passed}/${r.total}`; this.details = r.detail; }
    catch (e) { this.checkStatus = '检查未完成,可再次运行'; }
  }

  @Builder
  variable(name: string, value: string, enabled: boolean) {
    Row({ space: 8 }) {
      Circle().width(7).height(7).fill(enabled ? '#63D9AE' : '#A89BBE')
      Text(name).fontSize(13).fontFamily('monospace').fontColor('#C6C8D4')
      Text(value).fontSize(12).fontColor(enabled ? '#67DAB0' : '#9394A8')
    }.padding({ left: 12, right: 12, top: 8, bottom: 8 }).backgroundColor('#222438').borderRadius(6)
  }

  build() {
    Scroll() {
      Column({ space: 20 }) {
        Row({ space: 14 }) {
          Text('{ }').fontFamily('monospace').fontSize(29).fontWeight(FontWeight.Bold).fontColor('#B8A2F5')
          Column({ space: 4 }) {
            Text('表达式解剖台').fontSize(30).fontWeight(FontWeight.Bold).fontColor('#F4F0FA').width('100%')
            Text('better-parse · 一边看词元,一边看结构').fontSize(14).fontColor('#818199').width('100%')
          }.layoutWeight(1)
          Text('BOOLEAN').fontFamily('monospace').fontSize(11).letterSpacing(2).fontColor('#BCA5F4').padding(12)
            .border({ width: 1, color: '#454056' }).borderRadius(4)
        }.width('100%').padding({ bottom: 6 })

        Row({ space: 10 }) {
          ForEach(this.labels, (label: string, i: number) => {
            Button(`${i + 1}  ${label}`).id(`sample-${i}`).fontSize(13).layoutWeight(1).height(40)
              .fontColor(this.input === this.samples[i] ? '#DBCEF8' : '#9795AD')
              .backgroundColor(this.input === this.samples[i] ? '#3D305D' : '#242336').borderRadius(6)
              .onClick(() => { this.input = this.samples[i]; this.refresh(); })
          })
        }.width('100%')

        Column({ space: 0 }) {
          Row() {
            Text('expression.bool').fontFamily('monospace').fontSize(12).fontColor('#9890AC').layoutWeight(1)
            Text('!  →  &  →  |').fontFamily('monospace').fontSize(12).fontColor('#8B839E')
          }.width('100%').padding({ left: 18, right: 18, top: 13, bottom: 13 }).backgroundColor('#282536')
          Row({ space: 10 }) {
            Text('01').fontFamily('monospace').fontSize(14).fontColor('#5D576E').width(32)
            TextArea({ text: this.input, placeholder: 'online & !admin' }).id('source-input').maxLength(4096)
              .height(80).fontFamily('monospace').fontSize(23).fontColor('#E7D6FA').backgroundColor('#1C1A28')
              .layoutWeight(1).onChange((v: string) => { this.input = v; })
            Button('拆解').id('evaluate').fontSize(15).backgroundColor('#A58BDD').fontColor('#171126').height(44).borderRadius(8)
              .onClick(() => { this.refresh(); })
          }.padding({ left: 18, right: 18, top: 18, bottom: 18 }).backgroundColor('#1C1A28').width('100%')
        }.width('100%').borderRadius(10).clip(true).border({ width: 1, color: '#393445' })

        Row({ space: 12 }) {
          Text('环境').fontSize(12).fontColor('#7D778E')
          this.variable('online', 'true', true)
          this.variable('admin', 'false', false)
          this.variable('paid', 'true', true)
          Blank()
          Button('语法树视图').id('mode-0').fontSize(12).fontColor(this.mode === 0 ? '#E0D4FF' : '#8E899F')
            .backgroundColor(this.mode === 0 ? '#493664' : '#272436').borderRadius(6).onClick(() => { this.mode = 0; this.refresh(); })
          Button('词元详情').id('mode-1').fontSize(12).fontColor(this.mode === 1 ? '#E0D4FF' : '#8E899F')
            .backgroundColor(this.mode === 1 ? '#493664' : '#272436').borderRadius(6).onClick(() => { this.mode = 1; this.refresh(); })
        }.width('100%')

        if (this.view.ok) {
          Row({ space: 18 }) {
            Column({ space: 14 }) {
              Row() {
                Text('解析结构').fontSize(15).fontColor('#9B90B1').layoutWeight(1)
                Text(this.value(0)).id('result-0').fontFamily('monospace').fontSize(20).fontWeight(FontWeight.Bold)
                  .fontColor(this.value(0) === 'true' ? '#68D7AD' : '#D0A48C')
              }.width('100%')
              Text(this.printedTree()).id('ast-tree').fontFamily('monospace').fontSize(21).lineHeight(34).fontColor('#D3C3F2').width('100%')
              Text(this.value(2)).id('result-2').fontFamily('monospace').fontSize(12).fontColor('#89809A').width('100%').lineHeight(22)
            }.padding(26).backgroundColor('#242031').borderRadius(12).layoutWeight(1).alignItems(HorizontalAlign.Start)
            Column({ space: 12 }) {
              Text(this.mode === 1 ? '非空白词元' : '读这一条规则').fontSize(15).fontWeight(FontWeight.Medium).fontColor('#9C91B2').width('100%')
              if (this.mode === 1) {
                ForEach(this.view.rows.slice(3), (r: ResultRow, i: number) => {
                  Column({ space: 5 }) {
                    Text(r.label).fontSize(11).fontColor('#756986').width('100%')
                    Text(r.value).id(`result-${i + 3}`).fontSize(14).fontFamily('monospace').fontColor('#BFC0DC').width('100%')
                  }.width('100%').padding({ top: 10, bottom: 10 }).border({ width: { bottom: 1 }, color: '#373042' })
                })
              } else {
                Text(this.value(3)).id('result-3').fontFamily('monospace').fontSize(54).fontWeight(FontWeight.Bold).fontColor('#BBA0EE').width('100%')
                Text('个非空白词元').fontSize(13).fontColor('#7C738C').width('100%')
                Text('与先组合,或后组合;加上括号,根节点也会改变。').fontSize(15).fontColor('#A39BAC').lineHeight(26).width('100%').margin({ top: 12 })
              }
            }.padding(24).backgroundColor('#1D2130').borderRadius(12).width('39%').alignItems(HorizontalAlign.Start)
          }.width('100%').alignItems(VerticalAlign.Top)
        } else {
          Column({ space: 14 }) {
            Text('无法组成完整的表达式').fontSize(23).fontWeight(FontWeight.Bold).fontColor('#E6AD9E').width('100%')
            ForEach(this.view.rows, (r: ResultRow, i: number) => {
              Text(`${r.label}:${r.value}`).id(`result-${i}`).fontFamily('monospace').fontSize(14).fontColor('#BCA295').lineHeight(23).width('100%')
            })
          }.width('100%').padding(26).backgroundColor('#39282E').borderRadius(10)
        }
        Row({ space: 10 }) {
          Text(this.view.summary).id('summary').fontSize(13).fontColor(this.view.ok ? '#8CA999' : '#B48D82').layoutWeight(1)
          Text(this.checkStatus).id('check-status').fontSize(12).fontColor('#9990AA')
          Button('检查解析规则').id('run-checks').fontSize(12).backgroundColor('#373044').fontColor('#C1B1D8').borderRadius(6).onClick(() => { this.check(); })
        }.width('100%').padding({ top: 8, bottom: 8 })
        if (this.details.length > 0) { Text(this.details).id('check-details').fontFamily('monospace').fontSize(12).lineHeight(22).fontColor('#8F9A95').width('100%') }
      }.width('100%').padding(30).constraintSize({ maxWidth: 1240 })
    }.width('100%').height('100%').backgroundColor('#151521')
  }
}

6.4 源集、依赖和双架构链接配置

共享源码位置是 src/commonMain/kotlin。构建配置把 ARM64 与 X64 分开;每个目标当前需要 0 份显式 KLIB 依赖。没有外部依赖不代表可以使用桌面编译器,目标仍须是 OHOS。

harmony/native-config.json 完整内容:

{
  "common": "src/commonMain/kotlin",
  "code": "betterparse",
  "dependencies": {
    "ohos_arm64": [],
    "ohos_x64": []
  },
  "compilerOptions": [
    "-opt-in=kotlin.ExperimentalMultiplatform"
  ]
}

entry/src/main/cpp/CMakeLists.txt 把生成头文件与同 ABI 动态库配对,链接名称保持为 entry,对应页面的 libentry.so 导入:

cmake_minimum_required(VERSION 3.5.0)
project(harmony_library_example)
set(CMAKE_CXX_STANDARD 17)
if(CMAKE_OHOS_ARCH_ABI STREQUAL "x86_64" OR OHOS_ARCH STREQUAL "x86_64")
 set(KMP_ABI x86_64)
else()
 set(KMP_ABI arm64-v8a)
endif()
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include/${KMP_ABI})
add_library(kmp_library SHARED IMPORTED)
set_target_properties(kmp_library PROPERTIES IMPORTED_LOCATION ${CMAKE_CURRENT_SOURCE_DIR}/../../../libs/${KMP_ABI}/libbetterparse.so)
add_library(entry SHARED napi_init.cpp)
target_link_libraries(entry PUBLIC kmp_library libace_napi.z.so)

应用还需要启用外部 Native 构建,并声明本地接口类型依赖。下面依次是完整的 entry/build-profile.json5 与 entry/oh-package.json5,不包含个人签名:

{
  "apiType": "stageMode",
  "buildOption": {
    "resOptions": {
      "copyCodeResource": {
        "enable": false
      }
    },
    "externalNativeOptions": {
      "path": "./src/main/cpp/CMakeLists.txt",
      "abiFilters": [
        "arm64-v8a",
        "x86_64"
      ]
    }
  },
  "buildOptionSet": [
    {
      "name": "release",
      "arkOptions": {
        "obfuscation": {
          "ruleOptions": {
            "enable": false,
            "files": [
              "./obfuscation-rules.txt"
            ]
          }
        }
      }
    }
  ],
  "targets": [
    {
      "name": "default"
    },
    {
      "name": "ohosTest"
    }
  ]
}
{
  "name": "entry",
  "version": "1.0.0",
  "description": "better-parse · 词元、优先级和语法树",
  "main": "",
  "author": "",
  "license": "",
  "dependencies": {
    "libentry.so": "file:./src/main/cpp/types/libentry"
  }
}

6.5 从源码生成与页面一致的应用

按前文官方环境说明配置 JDK 17 与 OHOS Kotlin/Native 编译器,把 JAVA_HOME、KOTLIN_NATIVE_HOME 指向对应安装目录。在当前库目录运行下面这份完整脚本;它读取上面的配置,分别生成两个 ABI 的库和头文件,再复制进示例。脚本将公共源码和平台实现编译到明确的 OHOS 目标,并保留每个阶段的构建日志。

完整文件:harmony/build-native.py。

#!/usr/bin/env python3
"""Compile preserved common sources and the OHOS example bridge."""
from pathlib import Path
import argparse,os,json,subprocess,shutil,hashlib
ROOT=Path(__file__).resolve().parent.parent
CONFIG=json.loads((ROOT/'harmony/native-config.json').read_text(encoding='utf8'))
p=argparse.ArgumentParser();p.add_argument('--compiler',default=os.environ.get('KOTLIN_NATIVE_HOME'));p.add_argument('--sdk-native',default=os.environ.get('OHOS_NATIVE_HOME'));p.add_argument('--install');p.add_argument('--check-inputs',action='store_true');args=p.parse_args()
common=sorted((ROOT/CONFIG['common']).rglob('*.kt'));actual=sorted((ROOT/'harmony/src').rglob('*.kt'))
if CONFIG.get('nativeActual'):actual+=sorted((ROOT/CONFIG['nativeActual']).rglob('*.kt'))
extra=sorted((ROOT/'harmony/dependencies/src').rglob('*.kt')) if CONFIG.get('sourceDependency') else []
assert common and actual
if args.check_inputs:
    proof=json.loads((ROOT/'harmony/verification.json').read_text(encoding='utf8'))
    for path,digest in proof['native']['coreSourceHashes'].items():assert hashlib.sha256((ROOT/path).read_bytes()).hexdigest()==digest
    for target,abi in [('ohos_arm64','arm64-v8a'),('ohos_x64','x86_64')]:
        for name in CONFIG['dependencies'][target]:assert (ROOT/'harmony/dependencies'/name).is_file(),name
        assert hashlib.sha256((ROOT/'example/harmony/entry/libs'/abi/('lib'+CONFIG['code']+'.so')).read_bytes()).hexdigest()==proof['native']['outputs'][target]
    print('Shared source, dependencies and both compiled libraries match verification records');raise SystemExit(0)
if not args.compiler:p.error('Set KOTLIN_NATIVE_HOME or pass --compiler')
compiler=Path(args.compiler).resolve();env=dict(os.environ)
def run(target,phase,argv):
    dest=ROOT/'harmony/build'/target;dest.mkdir(parents=True,exist_ok=True)
    param=dest/(phase+'.args');param.write_text('\n'.join('"'+x.replace('\\','/')+'"' for x in argv),encoding='utf8')
    with (dest/(phase+'.log')).open('w',encoding='utf8') as log:q=subprocess.run(['cmd.exe','/d','/c',str(compiler/'bin/konanc.bat'),'@'+str(param)],env=env,stdout=log,stderr=subprocess.STDOUT)
    if q.returncode:raise SystemExit(f'{target} {phase} failed; inspect {phase}.log')
    print(target,phase,'passed',flush=True)
outputs={}
for target,abi in [('ohos_arm64','arm64-v8a'),('ohos_x64','x86_64')]:
    dest=ROOT/'harmony/build'/target;dest.mkdir(parents=True,exist_ok=True);name=CONFIG['code'];libs=[]
    for file in CONFIG['dependencies'][target]:libs+=['-library',str(ROOT/'harmony/dependencies'/file)]
    if CONFIG.get('utf8proc'):
        if not args.sdk_native:p.error('Unicode backend requires --sdk-native or OHOS_NATIVE_HOME')
        from build_utf8proc import prepare
        libs+=['-library',str(prepare(ROOT,compiler,env,target,Path(args.sdk_native).resolve()))]
    opts=['-target',target,'-produce','library','-Xmulti-platform','-Xexpect-actual-classes','-Xcommon-sources='+','.join(map(str,common+extra)),'-opt-in=kotlin.ExperimentalUnsignedTypes']+CONFIG.get('compilerOptions',[])+libs
    plugin=['-Xplugin='+str(ROOT/'harmony/dependencies/serialization-plugin.jar')] if CONFIG.get('serialization') else []
    if CONFIG.get('splitCore'):
        core=['-library',str(dest/(name+'_core.klib'))]
        run(target,'compile-core',list(map(str,common+extra+[a for a in actual if a.name!='Bridge.kt']))+opts+plugin+['-o',str(dest/(name+'_core'))])
        run(target,'compile-bridge',[str(ROOT/'harmony/src/Bridge.kt'),'-target',target,'-produce','library','-o',str(dest/name)]+plugin+core+libs)
        libs+=core
    else:run(target,'compile',list(map(str,common+extra+actual))+opts+plugin+['-o',str(dest/name)])
    run(target,'link',['-target',target,'-produce','dynamic','-o',str(dest/name),'-Xinclude='+str(dest/(name+'.klib'))]+libs)
    outputs[target]=hashlib.sha256((dest/('lib'+name+'.so')).read_bytes()).hexdigest()
    if args.install:
        project=Path(args.install).resolve();assert (project/'entry/build-profile.json5').is_file()
        for source,to in [(dest/('lib'+name+'.so'),project/'entry/libs'/abi/('lib'+name+'.so')),(dest/(name+'_api.h'),project/'entry/src/main/cpp/include'/abi/(name+'_api.h'))]:
            to.parent.mkdir(parents=True,exist_ok=True);shutil.copy2(source,to)
(ROOT/'harmony/build/manifest.json').write_text(json.dumps({'sourceFileCount':len(common),'outputs':outputs},indent=2),encoding='utf8')
python harmony/build-native.py --install example/harmony

完成原生构建后,用 DevEco Studio 打开 example/harmony,同步 entry 的类型依赖,为当前应用包名配置自己的开发者签名,再构建和安装 HAP。仅修改 ArkTS 可以重建应用;改过 Kotlin 或平台 actual 时,先重编两个 ABI 的动态库及匹配头文件。

本文的运行记录对应 API 24、OHOS Kotlin/Native 2.2.21-1.1.0,覆盖前面列出的输入与检查。API 26 和其他目标平台没有在这次记录中实测。更多通用三方库资料见 oh-tpc 社区。

7. 总结:别让一个布尔值藏住整条规则

这个例子更适合用来调试条件,而不是代替完整的权限系统。真实权限还会有身份来源、服务端校验和数据范围,不能只凭客户端输入的几个布尔变量作决定。示例使用固定变量,让讨论集中在词法和语法解析上。

接入 better-parse 后,最有用的不是把输入换算成真或假,而是能同时观察词元和结构。条件理解有分歧时,先确认字符如何分段,再确认节点如何组合,最后才看求值环境。这样的排查顺序对短表达式也有价值,尤其是那些看起来只需要补一层括号的问题。

源码见 better-parse 鸿蒙适配与示例,总项目见 harmonyos-crossplatform-libs。框架资料见 CPF-KMP-CMP,共建资料见 CPF-ApplicationTPC。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐