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. 03awaitとコールバック実行の説明
  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 ブリッジング

ArkCompiler ハイブリッドランタイムにおいて、ETS(ArkTS)の Promise と JavaScript の Promise をつなぐ 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

2 つの基本的な変換パス

パス 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 は Lock に MarkWord を使用するため、競合します
  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 には 2 つのパスがあります:

高速パス(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 は、PromiseInteropResolve/Reject ネイティブメソッドを呼び出す .then() コールバックを登録します 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(相互運用ハッシュ)と 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)

JS 固有のコールバックと Promise のリンク機能を備えるよう、JobQueue を拡張します。

メソッド説明
CreatePromiseLink(jsObject, etsPromise)JS Promise に C++ の then/catch コールバックを登録します
Post(fn, data)JS Promise を介してコールバックを JS ジョブキューに登録します

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
}

PromiseInterop.ets のコールバックから呼び出されるネイティブメソッド、PromiseInteropResolve() / PromiseInteropReject() から呼び出されます。

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 コードは、JSCONVERT_WRAP のスローパス中に C++ から 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 テストには、3 つの検証パターンがあります。

パターン戻り値時の ETS Promise の状態JSCONVERT_WRAP の経路JS による検証
A:ETS 内部で検証PENDINGスローパスETS のグローバル状態をポーリング
B:JS で解決を検証RESOLVEDファストパス返された Promise に対する JS の .then()
C:JS で保留を検証PENDINGスローパスJS の .then() + setTimeout による解決のトリガー

スレッド処理とコルーチンに関する考慮事項

  • 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 が値の型に適したコンバーターを見つけることを確認します

コメント