在 OpenHarmony 系统上,ArkTS 具备完整广泛的生态,为复用 ArkTS 生态,仓颉支持与 ArkTS 高效跨语言互通。
仓颉-ArkTS 互操作基于仓颉 CFFI 能力,通过调用 ArkTS 运行时接口,为用户提供库级别的 ArkTS 互操作能力。
一、使用场景:
1.在 ArkTS 应用开发仓颉模块:把用户仓颉代码封装成为 ArkTS 模块,能够被 ArkTS 代码加载和调用。
2.在仓颉应用里使用 ArkTS 存量库:在仓颉代码里创建新的 ArkTS 运行时,并加载和执行 ArkTS 的字节码。
二、互操作库的主要组成和功能:
JSValue: 统一的 ArkTS 数据类型,在跨语言调用中做传参,对 ArkTS 类型做判断和做数据转换。
JSContext: 一个 ArkTS 互操作上下文,用户创建 ArkTS 数据,辅助把 JSValue 转换为仓颉数据。
JSCallInfo: 一次 ArkTS 函数调用的参数集合,包含所有的入参和 this 指针。
JSRuntime: 一个由仓颉创建的 ArkTS 运行时。
三、在 ArkTS 应用里开发仓颉模块
开发仓颉互操作模块:
1.【仓颉侧】导入互操作库。

import ohos.ark_interop.*
2.【仓颉侧】定义要导出的函数,可被 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.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.

3.【仓颉侧】注册要导出的函数。

// 类名没有影响
class Main {
    // 定义静态构造函数(也可用全局变量和静态变量的初始化表达式触发)
    static init() {
        // 注册键值对
        JSModule.registerModule {context, exports =>
            exports["addNumber"] = context.function(addNumber).toJSValue()
        }
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

4.【ArkTS 侧】导入 ark_interop_loader,这是一个在 ohos-sdk 中提供的 napi 模块,作为仓颉运行时的启动器和仓颉模块的加载器。

import {requireCJLib} from “libark_interop_loader.so”
5.【ArkTS 侧】定义仓颉库导出的接口。

interface CangjieLib {
    // 定义的仓颉互操作函数,名称与仓颉侧注册名称一致。一般先定义 ArkTS 函数声明,在实现仓颉函数时根据声明来解析参数和返回。
    addNumber(a: number, b: number): number;
}
  • 1.
  • 2.
  • 3.
  • 4.

6.【ArkTS 侧】导入和调用仓颉库。

// 导入仓颉库,仓颉模块默认编译产物是 libentry.so,用户可以在 cjpm.toml 中修改配置。
const cjLib = requireCJLib("libentry.so") as CangjieLib;
// 调用仓颉接口
let result = cjLib.addNumber(1, 2);
console.log(`1 + 2 = ${result}`);
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

四、在仓颉应用里使用 ArkTS 模块
ArkTS 模块的编译产物主要有两种:
C 代码(+ArkTS)编译成 so。
纯 ArkTS 代码编译成 abc。
五、加载 ArkTS so 模块
(一)ArkTS so 模块根据部署方式的不同,分为以下几种:
随系统发布,在镜像的/system/lib64/module目录下。
随应用(hap)发布,在应用的/libs/arm64-v8a目录下,安装后在设备上的全局路径(通过hdc shell观察到的路径):/data/app/el1/bundle/public/${bundleName}/libs/arm64、沙箱路径(运行时可访问路径):/data/storage/el1/bundle/libs/arm64。
随动态库(hsp)发布。
(二)这里主要介绍怎么加载随系统发布的 so 模块,这些 so 在 OpenHarmony 的官方文档里会有开发文档。
接下来以相册管理模块作为示例,详细的介绍加载流程。
1.查看 ArkTS 文档,其导入模块的范本如下。

import photoAccessHelper from ‘@ohos.file.photoAccessHelper’;
2.创建 ArkTS 运行时,准备互操作上下文。

import ohos.ark_interop.*

func tryLoadArkTSSo() {
    // 创建新的 ArkTS 运行时
    let runtime = JSRuntime()
    // 获取互操作上下文
    let context = runtime.mainContext
    ...
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

3.根据 ArkTS 文档里模块导入名称,推导仓颉的模块导入参数。

鸿蒙原生应用元服务开发-仓颉ArkTS相互操作(一)_数据


4.导入 ArkTS 模块。

func tryLoadArkTSSo() {
    ...
    let module = context.requireSystemNativeModule("file.photoAccessHelper")
}
  • 1.
  • 2.
  • 3.
  • 4.

模块导入进来是一个 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相互操作(一)_加载_02


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

类型安全,特定类型的接口只能从安全引用里访问,总是需要先做显式的类型转换再访问。

生命周期安全,对于由 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()
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.

除了可以从对象上读取属性外,还可以对属性赋值或创建新属性,操作方式为 JSObject[key] = value,其中 key 可以是仓颉 String 、JSString 或 JSSymbol,value 是 JSValue 。
说明
通过 JSObject[key] = value 定义属性时,该属性可写、可枚举、可配置。
更多参见JavaScript 标准内置对象。
对属性赋值在以下几种场景会失败,失败之后没有异常或日志:
目标对象是 sealed 对象,由 Object.seal() 接口创建的对象具有不可修改的特性,无法创建新的属性和修改原有属性。
目标属性的 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)
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

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

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

    callback.call(thisArg: thisArg)
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

在 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()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

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

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())
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

资料来源:HarmonyOS Developer 官方网站