interop-promise について
interop-promiseは、ArkCompilerのハイブリッドランタイムでETS(ArkTS)とJavaScriptのPromise連携を扱う開発者向けガイドです。変換経路、ランタイム構成要素、awaitの仕組み、デバッグ上の要点を説明します。
主な機能
- 01ETSとJavaScript間の双方向Promise変換
- 02プロキシ作成とPromiseリンクの解説
- 03awaitとコールバック実行の説明
- 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 ブリッジング
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 / 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 |
2 つの基本的な変換パス
パス 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 は Lock に MarkWord を使用するため、競合します
- マッピングの登録:
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 には 2 つのパスがあります:
高速パス(Wrap の呼び出し時に EtsPromise がすでに解決済みの場合):
GetInteropObject()+HasReference()による同一性の確認napi_create_promise(env, &deferred, &jsPromise)— 保留中の JS Promise を作成しますLock()→!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 は、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 の境界を越える新しい相互運用型の追加
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が値の型に適したコンバーターを見つけることを確認します