关于 interop-promise
interop-promise 面向 ArkCompiler 混合运行时开发者,介绍 ETS(ArkTS)与 JavaScript 之间的 Promise 互操作。内容涵盖转换路径、运行时组件、await 机制及调试要点。
主要功能
- 01ETS 与 JavaScript Promise 双向转换
- 02讲解代理创建与 Promise 链接
- 03说明 await 和回调执行
- 04列出相关运行时源码文件
适用场景
- 01追踪跨语言边界的 Promise 转换
- 02排查被拒绝或未完成的互操作 Promise
- 03了解回调与协程行为
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ openharmonyinsight/ openharmony-skills --skill interop-promise安装后在 Claude Code 中输入/interop-promise确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
evals/evals.jsonREADME.mdreferences/api-reference.md详细介绍
Interop Promise - ETS/JS 跨语言 Promise 桥接
了解、开发和调试 Promise 互操作系统的指南。该系统可在 ArkCompiler 混合运行时中桥接 ETS(ArkTS)Promise 与 JavaScript Promise。
架构概览
Promise 互操作系统可在静态 ETS VM 与动态 JS VM 之间实现透明的双向 Promise 转换。当 ETS 调用 JS 异步函数(或反之)时,系统会创建代理对象并注册回调,以确保 resolve/reject 事件能正确跨语言边界传递。
ETS VM Side JS VM Side
────────────── ──────────────
EtsPromise JS Promise
│ │
├── interopObject_ → EtsPromiseRef ────────┤
├── linkedPromise_ → EtsPromiseRef │
├── event_ (await suspend/resume) │
├── mutex_ (thread safety) │
└── callbackQueue_ (.then handlers) │
│
SharedReferenceStorage ────────────────┘
(EtsPromiseRef ↔ JS Promise mapping)
JS→ETS path: JS Promise → JSCONVERT_UNWRAP(Promise) → EtsPromise proxy
ETS→JS path: EtsPromise → JSCONVERT_WRAP(Promise) → JS Promise
源代码位置
| 组件 | 路径 |
|---|---|
| JSConvertPromise(封装/解封装) | plugins/ets/runtime/interop_js/js_convert.h |
| EtsPromise 类 | plugins/ets/runtime/types/ets_promise.h/.cpp |
| EtsPromiseRef 桥接 | plugins/ets/runtime/types/ets_promise_ref.h |
| JsJobQueue / CreatePromiseLink | plugins/ets/runtime/interop_js/js_job_queue.h/.cpp |
| Promise 内置函数 | plugins/ets/runtime/intrinsics/std_core_Promise.cpp |
| SettleJsPromise / PromiseInterop | plugins/ets/runtime/interop_js/intrinsics_api_impl.cpp |
| PromiseInterop.ets | plugins/ets/stdlib/std/interop/js/PromiseInterop.ets |
| ETS Promise.ets | plugins/ets/stdlib/std/core/Promise.ets |
| CallJSHandler(ETS→JS 调用) | plugins/ets/runtime/interop_js/call/call_js.cpp |
| CallETSHandler(JS→ETS 调用) | plugins/ets/runtime/interop_js/call/call_ets.cpp |
| 类型路由(ConvertArgToEts/JS) | plugins/ets/runtime/interop_js/call/arg_convertors.h |
两种核心转换路径
路径 1:JS Promise → ETS 代理(JSCONVERT_UNWRAP)
当 ETS 调用返回 Promise 的 JS 函数时触发。
调用链:
CallJSHandler::Handle()
→ ConvertRetval() → ConvertArgToEts() → ConvertRefArgToEts()
→ JSConvertPromise::UnwrapImpl() [JSCONVERT_UNWRAP(Promise)]
UnwrapImpl 步骤:
- 身份检查:
SharedReferenceStorage::GetReference(env, jsVal)— 如果 JS Promise 已被封装,则复用现有代理 - 创建代理 EtsPromise:
EtsPromise::Create(coro)— STATE_PENDING - 创建 EtsPromiseRef 桥接:为什么需要?因为 SharedReferenceStorage 使用 MarkWord 进行互操作哈希,而 EtsPromise 使用 MarkWord 进行 Lock — 两者会冲突
- 注册映射:
SharedReferenceStorage::CreateJSObjectRef(ctx, ref, jsVal)— 建立 EtsPromiseRef ↔ JS Promise 双向映射 - 标记为代理:
hpromise->SetLinkedPromise(coro, href)— 在 await 中启用IsProxy()检查 - 创建链接:
EtsPromise::CreateLink()→JsJobQueue::CreatePromiseLink()— 在 JS Promise 的.then()上注册 C++ 回调
路径 2:ETS Promise → JS Promise(JSCONVERT_WRAP)
当 JS 调用返回 Promise 的 ETS 函数时触发。
调用链:
CallETSHandler::HandleImpl()
→ ConvertArgToJS() → ConvertRefArgToJS()
→ JSConvertPromise::WrapImpl() [JSCONVERT_WRAP(Promise)]
WrapImpl 有两条路径:
快速路径(调用 Wrap 时 EtsPromise 已完成):
- 通过
GetInteropObject()+HasReference()进行身份检查 napi_create_promise(env, &deferred, &jsPromise)— 创建待处理的 JS PromiseLock()→ 检查!IsPending() && !IsLinked()→ 快速路径- 通过
JSRefConvertResolve将 ETS 值转换为 JS 值 napi_resolve_deferred()或napi_reject_deferred()— 立即完成 JS Promise- 创建 EtsPromiseRef + 在 SharedReferenceStorage 中注册
慢速路径(EtsPromise 仍处于待处理状态):
1-2. 与快速路径相同
3. Lock() → IsPending() || IsLinked() → 慢速路径
4. Unlock() → 通过 Invoke 调用 PromiseInterop.connectPromise(promise, deferred)
5. connectPromise 注册 .then() 回调,以调用 PromiseInteropResolve/Reject 原生方法
6. 当 EtsPromise 后续完成时:OnPromiseCompletion() → LaunchCallback() → PromiseInteropResolve() → SettleJsPromise() → napi_resolve_deferred()
7. 创建 EtsPromiseRef + 在 SharedReferenceStorage 中注册
关键类
EtsPromise (ets_promise.h)
状态:STATE_PENDING(0)、STATE_RESOLVED(1)、STATE_REJECTED(2)、STATE_LINKED(3)
| 方法 | 描述 |
|---|---|
Create(coro) | 创建带有互斥锁和事件的 PENDING promise |
Resolve(coro, value) | 设置值,将状态转换为 RESOLVED,并调用 OnPromiseCompletion |
Reject(coro, error) | 设置错误,将状态转换为 REJECTED,并调用 OnPromiseCompletion |
Wait() | 通过 EtsEvent::Wait() 阻塞协程 |
IsProxy() | linkedPromise_ != nullptr — 对 JS Promise 代理返回 true |
IsPending/Resolved/Rejected/Linked() | 状态检查器 |
SubmitCallback(cb, workerDomain) | 将 .then 处理程序添加到 callbackQueue |
CreateLink(source, target) | 委托给 JobQueue::CreateLink() |
OnPromiseCompletion(coro) | 触发事件、启动排队的回调,并处理未处理的拒绝 |
LaunchCallback(coro, cb, groupId) | 在新协程中执行回调(PROMISE_CALLBACK 优先级) |
ChangeStateToPendingFromLinked() | 将状态从 LINKED 转换为 PENDING |
GetInteropObject() / SetInteropObject() | EtsPromiseRef 桥接对象 |
GetLinkedPromise() / SetLinkedPromise() | 用于检测代理 |
Lock() / Unlock() / IsLocked() | 通过 MarkWord 实现线程安全互斥锁 |
成员变量: value_、mutex_、event_、callbackQueue_、workerDomainQueue_、interopObject_、linkedPromise_、queueSize_、state_
EtsPromiseRef (ets_promise_ref.h)
精简的桥接对象,用于避免 SharedReferenceStorage(interop 哈希)和 EtsPromise(Lock)之间的 MarkWord 冲突。
class EtsPromiseRef : public EtsObject {
EtsObject *target_ {}; // Points to the actual EtsPromise
// MarkWord used by SharedReferenceStorage for interop hash index
};
JsJobQueue (js_job_queue.h/.cpp)
扩展 JobQueue,支持 JS 特定的回调和 Promise 链接。
| 方法 | 描述 |
|---|---|
CreatePromiseLink(jsObject, etsPromise) | 在 JS Promise 上注册 C++ then/catch 回调 |
Post(fn, data) | 通过 JS Promise 将回调发送到 JS job 队列 |
在 JS Promise 上注册的全局 C++ 回调:
OnJsPromiseResolved(env, info)→ 委托给OnJsPromiseCompleted(env, info, true)OnJsPromiseRejected(env, info)→ 委托给OnJsPromiseCompleted(env, info, false)OnJsPromiseCompleted(env, info, isResolved):将 JS 值转换为 ETS,调用EtsPromiseResolve或EtsPromiseReject
ETS Await 机制
ETS: await p;
→ EtsAwaitPromise(p)
→ IsProxy()?
→ YES: AwaitProxyPromise()
→ promise->Wait() // EtsEvent::Wait() — coroutine suspends
→ [JS resolves → OnJsPromiseCompleted → EtsPromiseResolve → Resolve → OnPromiseCompletion → Fire()]
→ Wait() returns
→ IsResolved()? return GetValue() : throw exception
→ NO: promise->Wait() // Direct ETS Promise await
要点: EtsAwaitPromise 首先通过 coro->GetManager()->Schedule() 让出 CPU,以便其他协程(包括 JS 微任务)在检查代理状态之前执行。
Promise 状态机
Create()
│
▼
STATE_PENDING
/ │ \
resolve() / CreateLink() \ reject()
/ │ \
▼ ▼ ▼
STATE_RESOLVED STATE_LINKED STATE_REJECTED
│
resolve() │ (from subscribeOnAnotherPromise)
▼
STATE_RESOLVED
- PENDING:初始状态,或调用 ChangeStateToPendingFromLinked() 后的状态
- LINKED:正在等待 JS 源 Promise 确定结果的代理 Promise
- RESOLVED/REJECTED:终止状态,value_ 保存结果
回调队列和执行
.then() 注册流程:
p.then(onResolve, onReject)
→ Promise.ets: thenImpl()
→ [native] EtsPromiseSubmitCallback(promise, callback, workerDomain)
→ SubmitCallback(): if settled → execute immediately; else → add to callbackQueue_
完成时执行:
OnPromiseCompletion(coro)
→ Fire() // Wake awaiters
→ for each callback in queue:
→ LaunchCallback(coro, callback, groupId)
→ Create CompletionEvent
→ coroManager->Launch(event, method, args, groupId, PROMISE_CALLBACK)
队列容量管理: 使用 EnsureCapacity() 动态调整大小,扩容策略为 2 * oldSize + 1。
SettleJsPromise — ETS→JS 桥接
从 ETS 完成 JS Promise 的最后一步:
void SettleJsPromise(EtsObject *value, napi_deferred deferred, EtsInt state)
{
// Must run on main worker thread
INTEROP_CODE_SCOPE_ETS_TO_JS(executionCtx);
// Convert ETS value to JS value
completionValue = refconv->Wrap(ctx, value);
// Complete the JS Promise
napi_resolve_deferred(env, deferred, completionValue); // or napi_reject_deferred
}
由 PromiseInteropResolve() / PromiseInteropReject() 调用;这些原生方法由 PromiseInterop.ets 回调调用。
PromiseInterop.ets — 慢速路径连接
final class PromiseInterop {
static connectPromise<T>(p: Promise<T>, deferred: long): void {
p.then<void, void>(
(value: T): void => { PromiseInterop.resolve<T>(value, deferred); },
(error: Any): void => { PromiseInterop.reject(error, deferred); }
);
}
private static native resolve<T>(value: T, deferred: long): void;
private static native reject(error: Any, deferred: long): void;
}
此 ETS 代码由 C++ 在 JSCONVERT_WRAP 慢速路径期间通过 PlatformTypes()->interopPromiseInteropConnectPromise->GetPandaMethod()->Invoke() 调用。
类型路由 — 如何触发 Promise 转换
Promise 转换并非由运行时类型检测(例如 napi_is_promise)触发,而是由 .d.ets 文件中的编译时类型签名驱动:
.d.ets: export declare function jsAsync(): Promise<string>;
↓ (compiler generates ProtoReader type info)
ProtoReader return type = EtsPromise class
↓ (runtime type routing)
ConvertRefArgToEts → JSRefConvertResolve(ctx, EtsPromise.RuntimeClass)
↓ (finds JSConvertPromise converter)
JSConvertPromise::UnwrapImpl() or WrapImpl()
如果 .d.ets 声明 Promise<T>,但 JS 返回的不是 Promise,则 Debug 模式下的 ASSERT(isPromise) 会失败。
返回值类型路由(快速路径参考)
| 方向 | 入口点 | 路由器 | 转换器 |
|---|---|---|---|
| JS→ETS(返回值) | CallJSHandler::ConvertRetval() | ConvertArgToEts() → ConvertRefArgToEts() | JSConvertPromise::UnwrapImpl() |
| ETS→JS(返回值) | CallETSHandler::ConvertArgToJS() | ConvertRefArgToJS() | JSConvertPromise::WrapImpl() |
| JS→ETS(参数) | CallETSHandler::ConvertArgs() | ConvertArgToEts() | JSConvertPromise::UnwrapImpl() |
| ETS→JS(参数) | CallJSHandler::ConvertArgsAndCall() | ConvertArgToJS() | JSConvertPromise::WrapImpl() |
测试模式
互操作 Promise 测试有三种验证模式:
| 模式 | 返回时 ETS Promise 状态 | JSCONVERT_WRAP 路径 | JS 验证 |
|---|---|---|---|
| A:ETS 内部验证 | PENDING | 慢速路径 | 轮询 ETS 全局状态 |
| B:JS 验证已解决 | RESOLVED | 快速路径 | 对返回的 Promise 调用 JS .then() |
| C:JS 验证待处理 | PENDING | 慢速路径 | JS .then() + setTimeout 触发 resolve |
线程与协程注意事项
- SettleJsPromise 必须在主工作线程执行(
ASSERT(IsMainWorker())) - Promise 回调 会以
PROMISE_CALLBACK优先级在新协程中启动 - 工作线程域:MAIN 或 GENERAL——会影响由哪个协程组处理回调
- 协程切换:
EtsAwaitPromise会在挂起前让出 CPU,以便处理 JS 微任务 - 互斥锁:EtsPromise 使用基于 MarkWord 的 Lock 来保证线程安全(因此需要 EtsPromiseRef)
- 事件:EtsEvent 为 await 提供协程挂起和恢复功能
常见任务
添加可跨越 Promise 边界的新互操作类型
- 在
js_convert.h中实现JSConvert<YourType>::Wrap()和UnwrapImpl() - 在
JSRefConvertResolve查找链中注册 - 在
OnJsPromiseCompleted和SettleJsPromise中处理转换(已通过JSRefConvertResolve通用处理)
调试卡住的 await
- 检查
EtsPromise::IsProxy()——它是 JS 代理吗? - 检查状态——卡在 PENDING 还是 LINKED?
- 确认
CreatePromiseLink已注册回调——检查是否调用了 JS Promise 的.then() - 检查是否调用了
OnJsPromiseCompleted——JS 微任务队列是否正在运行? - 检查是否调用了
EtsEvent::Fire()——是否执行了OnPromiseCompletion? - 检查协程调度——
Schedule()是否正确让出执行权?
调试类型转换失败
- 确认
.d.ets类型签名与实际 JS 返回类型一致 - 检查运行时的
ProtoReader类型信息 - 在 Debug 模式下:
UnwrapImpl中的ASSERT(isPromise)会捕获类型不匹配 - 检查
JSRefConvertResolve是否为该值类型找到了正确的转换器