mcpField
I
interop-promise

interop-promise

github.com/openharmonyinsight开发3541 次安装11 周前更新
代码仓库

关于 interop-promise

interop-promise 面向 ArkCompiler 混合运行时开发者,介绍 ETS(ArkTS)与 JavaScript 之间的 Promise 互操作。内容涵盖转换路径、运行时组件、await 机制及调试要点。

主要功能

  1. 01ETS 与 JavaScript Promise 双向转换
  2. 02讲解代理创建与 Promise 链接
  3. 03说明 await 和回调执行
  4. 04列出相关运行时源码文件

适用场景

  1. 01追踪跨语言边界的 Promise 转换
  2. 02排查被拒绝或未完成的互操作 Promise
  3. 03了解回调与协程行为

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/openharmonyinsight/openharmony-skills --skill interop-promise

安装后在 Claude Code 中输入/interop-promise确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

evals/
evals.json
README.md
references/
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 / CreatePromiseLinkplugins/ets/runtime/interop_js/js_job_queue.h/.cpp
Promise 内置函数plugins/ets/runtime/intrinsics/std_core_Promise.cpp
SettleJsPromise / PromiseInteropplugins/ets/runtime/interop_js/intrinsics_api_impl.cpp
PromiseInterop.etsplugins/ets/stdlib/std/interop/js/PromiseInterop.ets
ETS Promise.etsplugins/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 步骤:

  1. 身份检查:SharedReferenceStorage::GetReference(env, jsVal) — 如果 JS Promise 已被封装,则复用现有代理
  2. 创建代理 EtsPromise:EtsPromise::Create(coro) — STATE_PENDING
  3. 创建 EtsPromiseRef 桥接:为什么需要?因为 SharedReferenceStorage 使用 MarkWord 进行互操作哈希,而 EtsPromise 使用 MarkWord 进行 Lock — 两者会冲突
  4. 注册映射:SharedReferenceStorage::CreateJSObjectRef(ctx, ref, jsVal) — 建立 EtsPromiseRef ↔ JS Promise 双向映射
  5. 标记为代理:hpromise->SetLinkedPromise(coro, href) — 在 await 中启用 IsProxy() 检查
  6. 创建链接: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 已完成):

  1. 通过 GetInteropObject() + HasReference() 进行身份检查
  2. napi_create_promise(env, &deferred, &jsPromise) — 创建待处理的 JS Promise
  3. Lock() → 检查 !IsPending() && !IsLinked() → 快速路径
  4. 通过 JSRefConvertResolve 将 ETS 值转换为 JS 值
  5. napi_resolve_deferred() 或 napi_reject_deferred() — 立即完成 JS Promise
  6. 创建 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 边界的新互操作类型
  1. 在 js_convert.h 中实现 JSConvert<YourType>::Wrap() 和 UnwrapImpl()
  2. 在 JSRefConvertResolve 查找链中注册
  3. 在 OnJsPromiseCompleted 和 SettleJsPromise 中处理转换(已通过 JSRefConvertResolve 通用处理)
调试卡住的 await
  1. 检查 EtsPromise::IsProxy()——它是 JS 代理吗?
  2. 检查状态——卡在 PENDING 还是 LINKED?
  3. 确认 CreatePromiseLink 已注册回调——检查是否调用了 JS Promise 的 .then()
  4. 检查是否调用了 OnJsPromiseCompleted——JS 微任务队列是否正在运行?
  5. 检查是否调用了 EtsEvent::Fire()——是否执行了 OnPromiseCompletion?
  6. 检查协程调度——Schedule() 是否正确让出执行权?
调试类型转换失败
  1. 确认 .d.ets 类型签名与实际 JS 返回类型一致
  2. 检查运行时的 ProtoReader 类型信息
  3. 在 Debug 模式下:UnwrapImpl 中的 ASSERT(isPromise) 会捕获类型不匹配
  4. 检查 JSRefConvertResolve 是否为该值类型找到了正确的转换器

评论

支持 Markdown