仓颉-ArkTS 互操作开发规范
多引擎实例上下文敏感
【规则】 禁止跨引擎实例访问 JS 对象。
多引擎实例场景下,每个 JS 对象(如 JSValue 及其子类的实例)都绑定于创建它的引擎实例(JSContext)。不同引擎实例之间相互独立,不能共享 JS 对象。如果在非所属引擎中访问 JS 对象,可能会导致程序崩溃。
在仓颉- ArkTS 互操作库中,早期的访问 JS 对象的相关接口需要开发者手动传入 JSContext,调用时需要确保传入正确实例。这类接口已标记为“废弃(deprecated)”,建议统一使用不含 JSContext 参数的新接口,由接口实现本身来自动选择正确的引擎实例。
错误示例:
仓颉代码:
// 导入互操作库
import ohos.ark_interop.*
func doSth(context: JSContext, callInfo: JSCallInfo): JSValue {
// 创建新的运行时实例
let newRuntime = JSRuntime()
let newContext = newRuntime.mainContext
// 在新运行时上创建新对象
let newObjValue = newContext.object().toJSValue()
// 错误:将新对象的 JSValue 使用老运行时实例转换为对象
let newObj = newObjValue.asObject()
// 错误:在新对象设置属性时使用老运行时作为参数
newObjValue.setProperty(context, newContext.string("a"), newContext.boolean(false).toJSValue())
// 错误:在获取对象设置属性时使用老运行时创建的字符串作为键
newObjValue.getProperty(newContext, context.string("a"))
return newObjValue
}
let EXPORT_MODULE = JSModule.registerModule {
runtime, exports => exports["doSth"] = runtime.function(doSth).toJSValue()
}
正确示例:
仓颉代码:
// 导入互操作库
import ohos.ark_interop.*
func doSth(context: JSContext, callInfo: JSCallInfo): JSValue {
// 创建新的运行时实例
let newRuntime = JSRuntime()
let newContext = newRuntime.mainContext
// 在新运行时上创建新对象
let newObjValue = newContext.object().toJSValue()
// 正确:将新对象的 JSValue 使用新运行时实例转换为对象
let newObj = newObjValue.asObject()
// 正确:在新对象设置属性时使用非废弃接口(不显式传递context)
newObjValue.setProperty(newContext.string("a"), newContext.boolean(false).toJSValue())
// 正确:在获取对象设置属性时使用新运行时创建的字符串作为键
newObjValue.getProperty(newContext.string("a"))
return newObjValue
}
let EXPORT_MODULE = JSModule.registerModule {
runtime, exports => exports["doSth"] = runtime.function(doSth).toJSValue()
}
异常处理
【规则】 使用 try 语句捕获并处理跨语言调用异常。
在跨语言函数调用中,被调用侧(callee)抛出的异常会通过互操作库自动转换为调用侧(caller)可捕获的异常。调用侧应使用 try 语句进行异常捕获和处理,避免程序出错或崩溃。
正确示例(在 ArkTS 侧捕获仓颉异常):
仓颉侧代码:
// 导入互操作库
import ohos.ark_interop.*
func doSthWithException(context: JSContext, callInfo: JSCallInfo): JSValue {
if (callInfo.count > 0) {
throw Exception("should not pass any argument")
}
context.undefined().toJSValue()
}
let EXPORT_MODULE = JSModule.registerModule {
runtime, exports => exports["doSthWithException"] = runtime.function(doSthWithException).toJSValue()
}
ArkTS 侧代码:
interface CJLib {
doSthWithException(src?: string): void
}
function doSth(lib: CJLib): void {
// 在调用跨语言接口时,使用 try...catch 来捕获跨语言异常
try {
lib.doSthWithException("xxx")
} catch (err) {
// ...
}
}
正确示例(在仓颉侧捕获 ArkTS 异常):
仓颉侧代码:
// 导入互操作库
import ohos.ark_interop.*
func callArktsWithExp(context: JSContext, callInfo: JSCallInfo): JSValue {
// 在调用跨语言接口时,使用 try...catch 来捕获跨语言异常
try {
callInfo[0]
.asFunction()
.call()
} catch (err: JSCodeError) {
// ...
}
context.undefined().toJSValue()
}
let EXPORT_MODULE = JSModule.registerModule {
runtime, exports => exports["callArktsWithExp"] = runtime.function(callArktsWithExp).toJSValue()
}
ArkTS 侧代码:
interface CJLib {
callArkTSWithExp(callback: () => void): void
}
function doSth(lib: CJLib): void {
lib.callArkTSWithExp(() => {
throw new Error("this is an error")
})
}
正确的使用 JSContext.external 接口创建的 JS Object
【规则】 正确的使用 JSContext.external 接口创建的 JS Object。
通过 JSContext.external 创建的 JSExternal 对象,在 ArkTS 侧类型为 undefined,不应直接作为接口参数使用。建议将 JSExternal 对象绑定到一个 JSObject 上,通过对象封装隐藏内部数据,提升接口的安全性与可维护性。
错误示例:
仓颉侧代码:
// 导入互操作库
import ohos.ark_interop.*
// 定义共享类,SharedObject为互操作库的类
class Data <: SharedObject {
Data(
// 定义2个属性
var 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()
// 获取仓颉对象
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()
let data: Data = jsExternal.cast<Data>().getOrThrow()
let result = context.number(Float64(data.id)).toJSValue()
return result
}
}
仓颉侧代码对应的 ArkTS 接口声明:
export declare function createData(): undefined;
export declare function setDataId(data: undefined, value: number): void;
export declare function getDataId(data: undefined): number;
ArkTS 侧代码:
import { createData, setDatId, getDataId } from "libohos_app_cangjie_entry.so";
// 创建共享对象
let data = createData();
// 操作对象属性
setDataId(data, 3);
let id = getDataId(data);
console.log("id is " + id);
正确示例:
仓颉侧代码:
// 导入互操作库
import ohos.ark_interop.*
// 定义共享类
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()
// 从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()
let jsExternal = thisObject.getAttachInfo().getOrThrow()
let data = jsExternal.cast<Data>().getOrThrow()
let result = context.number(Float64(data.id)).toJSValue()
return result
}
}
仓颉侧代码对应的 ArkTS 接口声明:
export declare interface Data {
setId(value: number): void;
getId(): number;
}
export declare function createData(): Data;
ArkTS 侧代码:
import { createData } from "libohos_app_cangjie_entry.so";
// 创建共享对象
let data = createData();
// 操作对象属性
data.setId(3);
let id = data.getId();
console.log("id is " + id);
跨语言对象引用
【规则】 在跨语言传递对象时,开发者应避免本地代理对象持有对原生对象的引用,或在使用结束后及时将该引用置空,以防止内存泄漏。
在跨语言互操作过程中,容易出现跨语言对象引用成环的情况,进而导致相关对象无法被释放,造成内存泄漏。环形引用的根本原因在于,跨语言对象的代理(通常是跨语言方法的参数或返回值)与原生对象之间形成了环状依赖关系,而各自的垃圾回收机制(GC)无法自动识别并处理这种跨运行时的引用,从而需要开发者手动进行管理。

如上图所示,为规避此类问题,建议开发者在设计时尽量避免代理对象直接引用原生对象。如果业务场景确实需要代理对象持有原生对象的引用,请在使用完毕后及时释放对原生对象的引用关系。
环形引用错误示例:
仓颉侧代码:
import ohos.ark_interop.*
class CJData <: SharedObject {
let name: String
var callback: ?() -> Unit = None
init(name: String) {
this.name = name
}
}
func createCJData(context: JSContext, callInfo: JSCallInfo): JSValue {
let object = context.object()
let data = CJData(callInfo[0].toString())
object.attachCJObject(context.external(data))
object.defineOwnAccessor("name", getter: {
context, callInfo => context.string(data.name).toJSValue()
})
object.defineOwnAccessor(
"callback",
setter: {
context, callInfo =>
let callback = callInfo[0].asFunction()
data.callback = {
=> callback.call()
}
context.undefined().toJSValue()
}
)
object.toJSValue()
}
let EXPORT_MODULE = JSModule.registerModule {
runtime, exports => exports["createCJData"] = runtime.function(createCJData).toJSValue()
}
仓颉侧代码对应的 ArkTS 接口声明:
export declare interface CJData {
name: string;
callback: () => void;
}
export declare function createCJData(): CJData;
ArkTS 侧代码:
import { createCJData, CJData } from "libohos_app_cangjie_entry.so"
const data: CJData = createCJData("123")
data.callback = () => {
console.log(data.name)
}
上述例子中引用成环的原因如下:
- ArkTS 侧创建的 CJData 对象 data 底层通过 external 持有仓颉对象
- 仓颉对象(CJData 类型)持有 callback 变量
- callback 捕获 ArkTS 侧的 callback 函数
- ArkTS 侧的 callback 函数捕获了 ArkTS 侧创建的 CJData 对象 data
假设上述场景为业务场景需要,那么开发者需要在 data.callback 执行完成之后及时将 data.callback 置空,即可解除环形引用。示例如下:
// ...
data.callback()
data.callback = () = {}
// ...
ArkTS 主线程中调用的仓颉接口中,不能在主线程中阻塞等待 spawn(UIThread) 的执行结果
【规则】 ArkTS 主线程中调用的仓颉接口中,不能在主线程中阻塞等待 spawn(UIThread) 的执行结果,否则会造成死锁,触发 App Freeze 故障。
ArkTS 主线程中调用的仓颉接口时,仓颉代码中可能会通过 spawn(UIThread) 表达式向主线程抛一个异步任务,该操作通常用于将仓颉接口的执行结果返回给 ArkTS 侧。开发者需要注意,不能在主线程中阻塞等待 spawn(UIThread) 的执行结果,否则会造成死锁,触发 App Freeze 故障(APP_INPUT_BLOCK)。常见的阻塞行为包括但不限于:
- 使用 future.get() 等待 spawn(UIThread) 表达式返回值;
- 使用 Mutex 的 lock() 接口获取会在 spawn(UIThread) 的任务中释放的锁。
错误示例:
仓颉侧代码:
import ohos.ark_interop.*
import ohos.ark_interop_macro.*
import ohos.base.UIThread
@Interop[ArkTS]
public func testCJ(): Unit {
// ...
let future = spawn (UIThread) {
// ...
}
future.get() // 错误:spawn(UIThread) 是创建一个仓颉任务到主线程,future.get() 又在主线程等待,会造成死锁
// ...
}
ArkTS 侧代码:
import { testCJ } from "libohos_app_cangjie_entry.so"
@Entry
@Component
struct Index {
// ...
testCJ() // ArkTS 主线程中调用仓颉接口
// ...
}
仓颉与 ArkTS 互操作逻辑要求在与 ArkTS 运行时绑定的系统线程上执行
【规则】 在仓颉调用 ArkTS 时,所有涉及 ArkTS 数据访问或接口调用的操作,需要运行在 ArkTS 运行时绑定的系统线程上。否则将触发 JSThreadMisMatch 异常。
仓颉线程是用户态线程,运行时会将仓颉线程调度到系统线程上执行,因此仓颉程序默认不会绑定在特定系统线程执行;而仓颉与 ArkTS 互操作逻辑要求运行在与 ArkTS 运行时绑定的系统线程上,因此开发者进行互操作时,需要关注互操作发生的线程,如果在非 ArkTS 线程,开发者需要使用互操作库提供的接口切换到 ArkTS 线程执行。开发者可以使用以下接口来保证互操作逻辑的正确执行:
- 使用 JSContext.isInBindThread() 判断当前线程是否可以执行互操作接口;
- 如需切换线程执行,可使用:
- JSContext.postJSTask { ... } 创建在 ArkTS 线程执行的任务;
- 如果 ArkTS 被部署在主线程上,开发者可以使用 spawn(UIThread) 语法使互操作逻辑所在线程被调度到主线程执行、
错误示例:
仓颉代码:
import ohos.ark_interop.*
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()
// 新建仓颉线程
spawn {
// 实际仓颉函数行为
let value = a + b
// 创建result
let result = context.number(value).toJSValue() // 错误:没有运行在ArkTS运行时绑定的系统线程上
// 调用js回调
callback.call(result)
}
// 返回 void
return context.undefined().toJSValue()
}
let EXPORT_MODULE = JSModule.registerModule {
runtime, exports => exports["addNumberAsync"] = runtime.function(addNumberAsync).toJSValue()
}
正确示例(isInBindThread & postJSTask 使用示例):
仓颉代码:
import ohos.ark_interop.*
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()
// 新建仓颉线程
spawn {
// 实际仓颉函数行为
let value = a + b
if (context.isInBindThread()) { // 正确:如果当前线程为 ArkTS 运行时绑定的系统线程,可以直接同步调用
// 创建result
let result = context.number(value).toJSValue()
// 调用js回调
callback.call(result)
} else { // 正确:否则使用 postJSTask 发起异步回调至 ArkTS 线程上执行
context.postJSTask {
// 创建result
let result = context.number(value).toJSValue()
// 调用js回调
callback.call(result)
}
}
}
// 返回 void
return context.undefined().toJSValue()
}
let EXPORT_MODULE = JSModule.registerModule {
runtime, exports => exports["addNumberAsync"] = runtime.function(addNumberAsync).toJSValue()
}
正确示例(spawn(UIThread) 使用示例):
仓颉代码:
import ohos.ark_interop.*
import ohos.base.UIThread
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()
// 新建仓颉线程
spawn {
// 实际仓颉函数行为
let value = a + b
spawn (UIThread) { // 正确:调度到 ArkTS 主线程上执行
// 创建result
let result = context.number(value).toJSValue()
// 调用js回调
callback.call(result)
}
}
// 返回 void
return context.undefined().toJSValue()
}
let EXPORT_MODULE = JSModule.registerModule {
runtime, exports => exports["addNumberAsync"] = runtime.function(addNumberAsync).toJSValue()
}
仓颉应用中只能在主线程上使用 JSRuntime() 创建 ArkTS 运行时
【规则】 仓颉应用中只能在主线程上使用 JSRuntime() 创建 ArkTS 运行时。
线程环境要求 JSRuntime 绑定一个系统线程,所有互操作接口只能在这个系统线程上调用,否则会出现未定义的行为。然而仓颉的线程与系统线程不是 1:1 绑定的关系,导致在仓颉 spawn 出来的仓颉线程里创建的 JSRuntime,在仓颉视角里为同步调用,而在 ArkTS 的视角里会出现线程切换,进而触发未定义行为或崩溃。因此限制只能在系统线程上创建 JSRuntime 而不能在仓颉线程里创建 JSRuntime。
错误示例:
仓颉代码:
import ohos.ark_interop.*
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()
// 新建仓颉线程
spawn {
let runtime = JSRuntime() // 错误:只能在主线程上使用 JSRuntime() 创建 ArkTS 运行时
// ...
}
// 返回 void
return context.undefined().toJSValue()
}
let EXPORT_MODULE = JSModule.registerModule {
runtime, exports => exports["addNumberAsync"] = runtime.function(addNumberAsync).toJSValue()
}