仓颉语言与ArkTS互操作

时间:2025-01-26 09:18:01

在 OpenHarmony 系统上,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 = ()
    let b: Float64 = ()

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

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

    // 返回 JSValue
    return result
}
  1. 【仓颉侧】注册要导出的函数。

// 类名没有影响

class Main {
    // 定义静态构造函数(也可用全局变量和静态变量的初始化表达式触发)
    static init() {
        // 注册键值对
         {context, exports =>
            exports["addNumber"] = (addNumber).toJSValue()
        }
    }
}

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;
}

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

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

二、在仓颉应用里使用 ArkTS 模块

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

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

2.1 加载 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。
    随动态库(hsp)发布。
  3. 这里主要介绍怎么加载随系统发布的 so 模块,这些 so 在 OpenHarmony 的官方文档里会有开发文档。

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

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

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

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

三、在仓颉里操作 ArkTS 数据

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

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

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

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

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

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

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

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

ArkTS 类型 仓颉类型 安全引用 typeof 类型
undefined - JSUndefined
null - JSNull
boolean Bool JSBoolean
number Float64 JSNumber
string String JSString
object - JSObject
Array - JSArray
bigint BigInt JSBigInt
function - JSFunction
symbol - JSSymbol

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

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

3.1 操作 ArkTS 对象

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

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

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

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

  • 说明

通过 JSObject[key] = value 定义属性时,该属性可写、可枚举、可配置。

更多参见JavaScript 标准内置对象

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

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

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

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

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

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

3.2 调用 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 = (result).toJSValue()
    // 调用回调函数
    (retJSValue)
}

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

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

    (thisArg: thisArg)
}

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

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


let target = new Someone()

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

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

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

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

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

四、在 ArkTS 里操作仓颉对象

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

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

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

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

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

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

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

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

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

        let jsExternal = (context)

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

        let result = (Float64()).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_default.so") as CustomLib

// 创建共享对象
let data = ()
// 操作对象属性
(data, 3)
let id = (data)

("id is " + id)

JSExternal 对象在 ArkTS 里的类型会被识别为 undefined ,直接使用它来作为参数很容易被传递错误的参数。在使用前一个用例的仓颉库时:

...
// 创建共享对象
let data = ()
// 操作对象属性
(undefined, 3) // 错误的参数,应该传递的是仓颉引用,但是编译器能通过编译
let id = (data)
...

4.1 把仓颉对象的引用挂在 JSObject 上传递到 ArkTS

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

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

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

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

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

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

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

        return ()
    }

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

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

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

        let result = ()
        return ()
    }

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

        let result = (Float64()).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_default.so") as CustomLib

// 创建共享对象
let data = ()
// 操作对象属性
(3)
let id = ()

("id is " + id)

4.2 为仓颉共享对象创建 JSClass

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

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

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

            return clazz
        }
    }

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

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

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

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

        let result = ()
        return ()
    }

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

        let result = (Float64()).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_default.so") as CustomLib

// 创建共享对象
let data = new ()
// 操作对象属性
(3)
let id = ()

("id is " + id)

五、ArkTS 互操作和仓颉多线程

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

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

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

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

import {requireCJLib} from "libark_interop_loader.so"
// 定义导出的接口
interface CustomLib {
    addNumberAsync(a: number, b: number, callback: (result: number)=>void): void
}
// 导入仓颉库
const cjLib = requireCJLib("libentry_default.so") as CangjieLib;
// 调用仓颉函数
(1, 2, (result)=> {
    ("1 + 2 = " + result)
})
// 类名没有影响
class Main {
    // 定义静态构造函数
    static init() {
        // 注册键值对
        ("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 = ()
    let b: Float64 = ()
    let callback = (context)
    // 新建仓颉线程
    spawn {
        // 实际仓颉函数行为
        let value = a + b
        // 发起异步回调
         {
            // 创建result
            let result = (value)
            // 调用js回调
            (result)
        }
    }

    // 返回 void
    return ()
}

在 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 = ()
    // 创建新线程
    spawn {
        // 在新线程执行仓颉逻辑
        let result = a + b
        // 切换到ArkTS线程
         {
            // 在ArkTS线程执行resolve
            ((result).toJSValue())
        }
    }
    // 返回Promise
    ()
}
// ArkTS 调用
import {requireCJLib} from "libark_interop_loader.so"
// 定义导出的接口
interface CustomLib {
    addNumberAsync(a: number, b: number): Promise<number>
}

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

main()

相关文章