通过 JSObject[key] = value 定义属性时,该属性可写、可枚举、可配置。
更多参见JavaScript 标准内置对象。
在 OpenHarmony 系统上,ArkTS 具备完整广泛的生态,为复用 ArkTS 生态,仓颉支持与 ArkTS 高效跨语言互通。
仓颉-ArkTS 互操作基于仓颉 CFFI 能力,通过调用 ArkTS 运行时接口,为用户提供库级别的 ArkTS 互操作能力。
使用场景:
互操作库的主要组成和功能:
开发仓颉互操作模块:
【仓颉侧】导入互操作库。
import ohos.ark_interop.*
【仓颉侧】定义要导出的函数,可被 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 }
【仓颉侧】注册要导出的函数。
// 类名没有影响 class Main { // 定义静态构造函数(也可用全局变量和静态变量的初始化表达式触发) static init() { // 注册键值对 JSModule.registerModule {context, exports => exports["addNumber"] = context.function(addNumber).toJSValue() } } }
【ArkTS 侧】导入 ark_interop_loader,这是一个在 ohos-sdk 中提供的 napi 模块,作为仓颉运行时的启动器和仓颉模块的加载器。
import {requireCJLib} from "libark_interop_loader.so"
【ArkTS 侧】定义仓颉库导出的接口。
interface CangjieLib { // 定义的仓颉互操作函数,名称与仓颉侧注册名称一致。一般先定义 ArkTS 函数声明,在实现仓颉函数时根据声明来解析参数和返回。 addNumber(a: number, b: number): number; }
【ArkTS 侧】导入和调用仓颉库。
// 导入仓颉库,仓颉模块默认编译产物是 libentry_default.so,用户可以在 cjpm.toml 中修改配置。 const cjLib = requireCJLib("libentry_default.so") as CangjieLib; // 调用仓颉接口 let result = cjLib.addNumber(1, 2); console.log(`1 + 2 = ${result}`);
ArkTS 模块的编译产物主要有两种:
ArkTS so 模块根据部署方式的不同,分为以下几种:
这里主要介绍怎么加载随系统发布的 so 模块,这些 so 在 OpenHarmony 的官方文档里会有开发文档。
接下来以相册管理模块作为示例,详细的介绍加载流程。
查看 ArkTS 文档,其导入模块的范本如下。
import photoAccessHelper from '@ohos.file.photoAccessHelper';
创建 ArkTS 运行时,准备互操作上下文。
import ohos.ark_interop.* func tryLoadArkTSSo() { // 创建新的 ArkTS 运行时 let runtime = JSRuntime() // 获取互操作上下文 let context = runtime.mainContext ... }
根据 ArkTS 文档里模块导入名称,推导仓颉的模块导入参数。
ArkTS 导入名 | 仓颉导入参数 | 说明 |
---|---|---|
@ohos.file.photoAccessHelper | ("file.photoAccessHelper") | 以 @ohos 开头,那么参数只需要去掉 "@ohos."。 |
@hms.core.push.pushService | ("core.push.pushService", prefix: "hms") | 以非 @ohos 开头,那么参数去掉 "@xxx.",并把 xxx 作为第二个参数。 |
导入 ArkTS 模块。
func tryLoadArkTSSo() { ... let module = context.requireSystemNativeModule("file.photoAccessHelper") }
模块导入进来是一个 JSValue,接下来可以按照操作 ArkTS 数据的方法去操作模块。
从 ArkTS 传过来的参数,其原始类型是JSValue,这是一个匿名类型的数据,首先需要知晓其类型。
当知道其类型之后,再把JSValue转换为对应的仓颉类型或 ArkTS 引用。
通过仓颉数据来构造 ArkTS 数据,是通过 JSContext 的方法类构造 ArkTS 数据。
一个应用进程可以存在多个 ArkTS 运行时,而 ArkTS 运行时之间的数据是不通用的,任何 ArkTS 数据都归属于一个特定的运行时,因此创建 ArkTS 数据接口是从运行时的角度出发。
以number举例,创建一个number的方式是JSContext.number(Float64)。
ArkTS 主要数据类型对应到仓颉类型的映射如下:
ArkTS 类型 | 仓颉类型 | 安全引用 | typeof 类型 |
---|---|---|---|
undefined | - | JSUndefined | JSType.UNDEFINED |
null | - | JSNull | JSType.NULL |
boolean | Bool | JSBoolean | JSType.BOOL |
number | Float64 | JSNumber | JSType.NUMBER |
string | String | JSString | JSType.STRING |
object | - | JSObject | JSType.OBJECT |
Array | - | JSArray | JSType.OBJECT |
bigint | BigInt | JSBigInt | JSType.BIGINT |
function | - | JSFunction | JSType.FUNCTION |
symbol | - | JSSymbol | JSType.SYMBOL |
安全引用的安全体现在两个方面:
从一个互操作函数的实现举例,该函数在 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 标准内置对象。
对属性赋值在以下几种场景会失败,失败之后没有异常或日志:
对于一个未知对象,可以枚举出该对象的可枚举属性:
func handleUnknownObject(context: JSContext, target: JSObject): Unit { // 枚举对象的可枚举属性 let keys = target.keys() println("target keys: ${keys}") }
创建一个新的 ArkTS 对象,可以通过 JSContext.object() 来创建。
对于 ArkTS 运行时,有一个特殊的对象,该对象是 ArkTS 全局对象,在任何 ArkTS 代码里都可以直接访问该对象下的属性,在仓颉侧可以通过 JSContext.global 来访问它。
拿到一个 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 运行时的内存管理机制来控制仓颉对象的生命周期,并通过相关的互操作接口来访问该对象。
// 定义共享类 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_default.so") as CustomLib // 创建共享对象 let data = cjLib.createData() // 操作对象属性 cjLib.setDataId(data, 3) let id = cjLib.getDataId(data) console.log("id is " + id)
JSExternal 对象在 ArkTS 里的类型会被识别为 undefined ,直接使用它来作为参数很容易被传递错误的参数。在使用前一个用例的仓颉库时:
... // 创建共享对象 let data = cjLib.createData() // 操作对象属性 cjLib.setDataId(undefined, 3) // 错误的参数,应该传递的是仓颉引用,但是编译器能通过编译 let id = cjLib.getDataId(data) ...
在实际开发接口时,可以把 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_default.so") as CustomLib // 创建共享对象 let data = cjLib.createData() // 操作对象属性 data.setId(3) let id = data.getId() console.log("id is " + id)
把所有的对象操作方法直接挂在对象上,一方面占用内存比较大,另一方面创建对象的开销比较大。对于追求性能的场景,可以定义一个 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_default.so") as CustomLib // 创建共享对象 let data = new cjLib.Data() // 操作对象属性 data.setId(3) let id = data.getId() console.log("id is " + id)
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; // 调用仓颉函数 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_default.so") as CangjieLib; // 调用仓颉函数 let result = await cjLib.addNumberAsync(1, 2) console.log("1 + 2 = " + result) } main()