Swift Digest
Blog | Swift.org Blog

どこでも Swift: 相互運用性を活かして Windows 向けに開発する

Swift Everywhere: Using Interoperability to Build on Windows

このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら

この記事の要点

背景: Swift の相互運用は clang を介する

2 つの言語が相互運用する場合、その境界での関数呼び出し(Foreign Function Interface、FFI)は、libffi のようなライブラリを使って C を経由するのが伝統的でした。この方式には実行時性能のコストや、余分なボイラープレートが生じるという欠点があります。

Swift は代わりに、C / C++ コンパイラである clang のコピーを組み込んでいます。clang が言語間を直接変換するため、コードサイズと実行時性能のペナルティを避けられます。この相互運用性は既存システムとよく馴染み、既存の C ライブラリの上に複雑なソフトウェアを構築できます。

Windows API へアクセスする

リッチでネイティブなアプリケーションを作るとき、相互運用の重要な用途の 1 つがプラットフォーム固有 API の呼び出しです。Windows API は長い歴史を反映して後方互換性を保ち続けてきた結果、さまざまな形の API が積み重なっており、その多くは古く低レベルで、C で定義されています。

Swift は C の関数やデータ型へのアクセスに libffi ではなく clang を使うため、clang の (ヘッダ)モジュール という機能を利用します。clang モジュールは一連の宣言をまとめ、どの宣言がどのライブラリに属し、どのモジュールに依存し、どの言語向けかを示します。これは module.modulemap という補助ファイルでモジュールを定義することで実現されます。

Windows API へアクセスするには、Windows SDK を 1 つ以上の clang モジュールに「モジュール化」する必要があります。Swift ツールチェインには Windows SDK のモジュール定義が WinSDK clang モジュール として含まれており、さらに Swift モジュールがその上に重なって、一部の定義をより Swift らしく整えています。これにより Windows SDK の C API が利用でき、最新の API すべてを含むわけではないものの、コマンドラインアプリや GUI アプリを Windows 上で構築できます。

最新の API は C だけでは公開されておらず、Windows SDK の多くの部分は C++ として公開されています。Swift 5.9 では言語レベルの相互運用を C++ にも拡張するサポートが導入されました。仮想メソッドや copyable な型はまだ利用できないものの、C++ Interop が成熟するにつれて、Swift から使えるネイティブなプラットフォーム API も Windows SDK の C++ API の大半を含むように広がっていきます。

この C++ Interop により、プラットフォーム API にとどまらず、C++ コミュニティが長年かけて書いてきた高性能・クロスプラットフォームなライブラリ群も Swift から利用できるようになります。たとえば Firebase はクラウドサービスとして広く使われており、Apple プラットフォーム向けの(Objective-C ベースの)Swift SDK に加えてクロスプラットフォームな C++ SDK が提供されています。C++ Interop により、この C++ SDK を Swift から使う橋渡し(swift-firebase)が構築されつつあります。

COM(Component Object Model)とは

ライブラリはコードを共有する 1 つの手段ですが唯一ではありません。別のスタイルとして、2 つの独立したアプリケーションが互いに機能を公開し合う プロセス間通信(IPC) があります。Windows でこの手法を実装した代表例が COM(Component Object Model) です。COM は 1990 年代の OLE(Object Linking and Embedding)から発展したもので、よく定義されたインターフェイス(たとえば IOleObject)を実装してプロセスをまたいで実装を共有できるようにします。

COM の設計は柔軟かつ強力で、Windows 以外にも広く採用されました。CoreFoundation はプラグインモデルに採用し、CFLite は COM の実装を Linux にもたらし、Mozilla の XPCOM や IOKit のドライバモデルなどにも影響を与えています。

COM の核心は、機能を公開する インターフェイス(通常は IDL で定義)です。インターフェイスはグローバルに一意な ID で識別され、すべて基底インターフェイス IUnknown を継承します。IUnknown は COM の 2 つの基本操作を公開します。

  1. オブジェクトの寿命管理
  2. オブジェクトの機能へのアクセス

Swift と同じく、寿命管理は参照カウントで実装され、COM では AddRef / Release メソッドとして公開されます。機能へのアクセスは QueryInterface メソッドで実装され、利用側がオブジェクトの機能を動的に要求できます。動的に問い合わせるためビルド時に操作を静的には特定できませんが、コストはポインタの間接参照数回(C++ の仮想メソッド程度)で、性能オーバーヘッドはごくわずかです。

C 相互運用による COM サポート

COM は動的に扱うためのインターフェイスであると同時に、パラメータの渡し方や関数呼び出しの並びを定める ABI(Application Binary Interface) でもあります。Swift から COM インターフェイスと通信するには、この ABI 要件に従う必要があります。FFI の共通語は C なので、COM の ABI は C で表現できます。C で IUnknown は次のようになります。

typedef struct IUnknownVtbl {
  ULONG (STDMETHODCALLTYPE *AddRef)(IUnknown *pUnk);
  ULONG (STDMETHODCALLTYPE *Release)(IUnknown *pUnk);
  HRESULT (STDMETHODCALLTYPE *QueryInterface)(IUnknown *pUnk, REFIID riid, void **ppvObject);
} IUnknownVtbl;

struct IUnknown {
    const struct IUnknownVtbl *lpVtbl;
} IUnknown;

これを Swift で表現すると、いくつかの制約を持つプロトコルになります。

public typealias REFIID = UnsafePointer<IID>

public protocol IUnknown: class {
  class var IID: IID { get }

  func AddRef() -> ULONG
  func Release() -> ULONG
  func QueryInterface(_ riid: REFIID, _ ppvObject: UnsafeMutablePointer<UnsafeMutableRawPointer?>?) -> HRESULT
}

extension IUnknown {
  func QueryInterface<Interface: IUnknown>() throws -> Interface? {  }
}

プロトコル宣言の : class は、適合する型が Swift のクラスでなければならないことを示すクラス制約です。ここに IUnknown と Swift のクラス制約付き型との意味的な対応が見て取れます。Swift のクラスは ARC で参照カウントを行い、COM は MRC(手動の参照カウント)で同じことを行うので AddRef / Release が対応します。残る QueryInterface は COM インターフェイスを動的に問い合わせるもので、Swift のキャスト操作に対応します。つまり概念的には、IUnknown は「別のどこかで実装された Swift のクラス型を持っている」と言っているのと同じです。

COM の概念が Swift にきれいに対応するため、COM と Swift の橋渡しを構築できます。関連コードは Swift/COM で公開されており、たとえば Windows の 3D アクセラレーション API である DirectX(C++ と COM のインターフェイス群として公開)を使って、シェーダ付きの 3D 立方体を描く DXSample が実例になっています。

他者(たとえば DirectX)が実装したインターフェイスを使うとき、受け取るのは IUnknown への生ポインタです。生ポインタのまま扱うのは煩雑なので、ポインタをラップして間接参照を抽象化すると COM が扱いやすくなります。

open class ID3D12Object: IUnknown {
  public override class var IID: IID { IID_ID3D12Object }

  public func SetName(_ Name: String) throws {
    _ = try perform(as: WinSDK.ID3D12Object.self) { pThis in
      try CHECKED(pThis.pointee.lpVtbl.pointee.SetName(pThis, $0))
    }
  }
  
}

逆に、COM インターフェイスを Swift で実装して C / C++ から呼び出させることも可能です(このサポートは初期段階で、C++ Interop の進展とともに発展します)。COM は厳格な ABI を持つため、オブジェクトを適切に構築できれば、言語境界を越えて任意の COM クライアントに渡せます。

C++ 相互運用による COM サポート

進化中の C++ Interop は、COM と Swift の橋渡しをよりシンプルにします。COM のインターフェイスモデルは C++ のクラスに非常に近く、COM インターフェイスは C++ クラスに、各メソッドは C++ 型の仮想メソッドに直接対応します。DirectX のように COM 型として公開される Windows API は、主に C++ クラスとして公開されています。Swift の C++ Interop が改善すれば、COM インターフェイスを C++ クラスとして import し、自然に Swift 型へ橋渡しできるようになり、C 経由のボイラープレートを削減できます。本記事執筆時点では、仮想メソッドのディスパッチに対する C++ Interop サポートは開発中です。

C++ Interop の取り組みは、別の面でも COM との橋渡しを助けています。SWIFT_SHARED_REFERENCE アノテーションにより、参照カウントされる外部型を Swift でサポートできるようになりました。COM は参照カウントされたインターフェイスを提供するので、COM インターフェイスに SWIFT_SHARED_REFERENCE を付けることで ARC を活用してメモリ管理を自動化でき、一群のメモリ安全性の問題を避けられます。

COM への Swift 言語サポートを改善する

現状の C++ Interop の制限により、COM インターフェイスを橋渡しする際は共通項である C にフォールバックする必要があり、ラッパー型の実装には大量のボイラープレートが伴います。これは「明快で表現力の高いコード」という Swift の目標に反します。そこで、アノテーションによって COM をより良くサポートするよう Swift 言語を拡張する案が考えられており、正式な evolution proposal に値するアイデアとして挙げられています。たとえば次のように属性を付けるだけで、型を COM からアクセス可能にできる姿が構想されています。

@COM(IID: IID_ICustomInterface, CLSID: CLSID_CustomInterface)
open class CCustomInterface: ICustomInterface {
  override open func QueryInterface<Interface: IUnknown>() throws -> Interface? {
    switch riid.pointee {
    case IID_ICustomInterface, IID_IUnknown:
      return Unmanaged<Self>.passRetained(self)
    default:
      return nil
    }
  }
  
}

Swift マクロでボイラープレートの一部を軽減できますが、Swift 型を本当に COM へ橋渡しするにはオブジェクトのメモリレイアウトを考慮する必要があります。COM は ABI なのでメモリ上の表現が COM の規定と一致しなければならず、レイアウトの制御はマクロだけでは実現できません。1 つの実現案として、Swift のオブジェクトレイアウトの一部(オプトイン)として、COM の ABI 要件に適合する領域を追加し、vtable を手作業で再構築せずに透過的に橋渡しする方法が考えられます。

進行中の distributed actor の取り組みも COM とよく整合します。Distributed COM(DCOM)は COM オブジェクトにネットワーク透過性を与えます。distributed actor は通信フォーマットを規定しないため、DCOM の標準的な通信プロトコル(DCE/RPC)を再利用することもでき、Windows 上のネイティブな Swift アプリケーションをコマンドラインから大規模分散システムまで容易にスケールできるようになるかもしれません。

まとめ: Windows での相互運用

Swift の相互運用ツールは、既存プラットフォーム上でリッチなネイティブアプリやライブラリを構築するのに有力で、メモリ安全性と使い勝手の面で C / C++ の優れた代替になります。Windows では特に、相互運用機能によってシステム API の非常に広い範囲へアクセスできます。さらに、COM は Windows 以外でも使われているため、上記のようなネイティブな COM 橋渡しを含む Windows システム API 連携の改善は、他のプラットフォームにも役立ちます。C++ Interop・マクロ・distributed actor といった新機能は、よりポータブルにアプリケーションを書くための新たな可能性を切り開いています。

関連リンク