Swift 6.3以降では、グローバル関数に@cを付け、互換ヘッダーを生成し、呼び出し側をCとしてコンパイルして、swiftcでリンクします。この手順では、各境界を実演する小さなネイティブMacコマンドラインプログラムを作成します。
ここで扱う課題は明確です。通常の.cファイルをObjective-CやC++としてコンパイルせずに、Swiftの計算処理を呼び出します。Swiftの公式リリースドキュメントによると、@cはSwift 6.3で導入された、サポート対象のC相互運用機能です。実験的な属性についての古い情報を参考にする前に、インストール済みのコンパイラを確認してください。Swift 6.3 Released
Swift関数をCから呼び出せるようにするには?
宣言をCコンパイラが読み取れること、実装がCの呼び出し規約を使うこと、リンカーにオブジェクトファイルを渡すことの3点が一致している必要があります。ヘッダーが解決するのは最初の点です。ヘッダーだけでは、実装の不足を補ったり、既存の関数のABIを変更したりすることはできません。
@c属性を付けると、グローバルなSwift関数はCの呼び出し規約を使い、そのシグネチャはCで表現可能な型に制限されます。属性の引数は任意で、C側の名前を指定します。Swiftは、生成する互換ヘッダーのC互換部分に対応する宣言を出力します。これらの仕様は、実装済みのSwift Evolution提案に記載されています。C互換の関数と列挙型
この例では、インターフェイスがCの整数を受け取り、返します。サンプルの計算処理は、表示用のパーセンテージを0から100までの範囲に収めます。バッファーや確保した戻り値、オブジェクトのライフタイムについて調整する必要はありません。そのため、最初のビルドではコンパイルとリンクに集中できます。
まずMacのツールチェーンを確認する
Clang、macOS SDK、Swift 6.3以降を備えたmacOS開発環境が必要です。AppleはXcodeにコマンドラインツールを同梱しており、別途提供されているCommand Line Toolsパッケージも利用できます。すでにXcodeがある場合、別のツールをインストールする必要はないとAppleは説明しています。コマンドラインツールをインストールする
ターミナルで次の確認を実行します。
xcode-select --print-path
xcrun --sdk macosx swiftc --version
xcrun --sdk macosx clang --version
Swiftコンパイラ自体のバージョンを確認してください。新しいSDKをインストールしても、古いコンパイラで@c機能が使えるようにはなりません。Xcodeのバージョンが複数インストールされている場合、Appleのドキュメントでは、Xcode > Settings > Locations > Command Line Toolsで使用するバージョンを選択する方法と、シェルセッション内でDEVELOPER_DIRを使って切り替える方法が説明されています。コマンドラインツールの設定を構成する
以下のコマンドでは、選択中の開発ツールを呼び出すためにxcrunを使用します。Appleは、コマンドラインからのビルドに関するリファレンスで、このツールの検出機能について説明しています。コマンドラインからのビルドに関するXcode FAQ
作業用の空のディレクトリを作り、以下の2つのソースファイルをその中に保存してください。そのディレクトリからビルドコマンドを実行します。これはローカルのMac向けのネイティブビルドです。ユニバーサルバイナリの作成や、別のプラットフォーム向けの設定は行いません。
Swiftの実装とCの呼び出し側を書く
公開するSwift関数を定義する
次のSwiftソースファイル全体をPercent.swiftとして保存します。
@c(demo_clamp_percent)
public func clampPercent(_ value: CInt) -> CInt {
if value < 0 {
return 0
}
if value > 100 {
return 100
}
return value
}
Swift側の名前はclampPercent、C側の名前はdemo_clamp_percentです。CIntはCのintに対応します。この明示的な対応付けを使えば、SwiftのIntがCのintを意味すると決めつけずに済みます。SwiftのIntに関するドキュメント上の対応型はptrdiff_tです。C互換の関数と列挙型
この関数は値を比較し、入力値または定数のいずれかを返します。パーセンテージの範囲外の値を含め、計算で算術オーバーフローは発生しません。公開名の接頭辞は、大きなプログラム内でこのインターフェイスを他の関数と区別するための命名規則の例です。
完全なCプログラムから呼び出す
このファイルをmain.cとして保存します。ビルドで次のインクルード先ヘッダーが生成されます。
#include <stddef.h>
#include <stdio.h>
#include "Percent-Swift.h"
int main(void) {
const int inputs[] = {-12, 42, 140};
const int expected[] = {0, 42, 100};
const size_t count = sizeof inputs / sizeof inputs[0];
for (size_t i = 0; i < count; ++i) {
const int actual = demo_clamp_percent(inputs[i]);
printf("%d -> %d\n", inputs[i], actual);
if (actual != expected[i]) {
fprintf(stderr, "Unexpected result at index %zu\n", i);
return 1;
}
}
return 0;
}
3つの入力で、下限、値が変わらないケース、上限を確認します。プログラムは結果を明示的に検証し、不一致があれば0以外のステータスを返します。そのため、単に数値を表示するだけの例よりも役立ちます。
ヘッダーを生成し、Cをコンパイルしてプログラムをリンクする
2つのファイルがあるディレクトリから、シェルで次のコマンドを順に実行します。各ビルド手順が失敗すると、そこで処理が停止します。
(
set -eu
SDK_PATH="$(xcrun --sdk macosx --show-sdk-path)"
xcrun --sdk macosx swiftc \
-sdk "$SDK_PATH" \
-parse-as-library \
-module-name Percent \
-emit-object Percent.swift \
-emit-objc-header-path Percent-Swift.h \
-o Percent.o
xcrun --sdk macosx clang \
-isysroot "$SDK_PATH" \
-x c -std=c11 \
-Wall -Wextra -Werror \
-c main.c -o main.o
xcrun --sdk macosx swiftc \
-sdk "$SDK_PATH" \
main.o Percent.o \
-o percent-demo
./percent-demo
)
最初のコマンドで、Swiftのオブジェクトファイルと互換ヘッダーが生成されます。Swiftドライバーでは、-emit-objectはオブジェクトファイルの出力、-parse-as-libraryはソースをスクリプトではなくライブラリとして解析するオプションです。これにより、実行可能ファイルのエントリーポイントはCファイル側に置かれます。Swiftコンパイラドライバーのオプション
-emit-objc-header-pathは歴史的な名前ですが、@c宣言について互換ヘッダーのCセクションを生成するよう要求するオプションでもあります。実装済みの提案では、この用途で同フラグを使えることが明記されています。推測したプロトタイプで生成済みヘッダーを手動で置き換えないでください。C互換の関数と列挙型
2つ目のコマンドで、ソース言語を正しく指定できているかを確認します。Clangのドキュメントでは、-xは入力言語の選択、-cはリンクを行わずにオブジェクトファイルを生成するオプションとされています。ここでは-x c -std=c11により、実際に生成したヘッダーを読み込みながら、呼び出し側をC11としてコンパイルします。Clangコマンドガイド
最後のコンパイラ呼び出しでは、Swiftドライバーを使って両方のオブジェクトファイルをリンクします。Swiftのコンパイラガイドでも、低レベルのリンクコマンドを手作業で組み立てずに、ファイルのリンクをswiftcに依頼するよう推奨しています。コンパイラのデバッグ
この例の分岐から導かれる出力は次のとおりです。
-12 -> 0
42 -> 42
140 -> 100
これらのコマンドは、参照したドキュメントと照合しましたが、ここでは実行していません。調査環境はLinuxで、swiftcもClangも利用できないためです。上記の出力は期待される結果であり、Macでの実行テストの報告ではありません。
既存コードに合ったインターフェイスを選ぶ
Swift 6.3以降で小規模なインターフェイスを新たに作る場合、上記の生成ヘッダーを使う方法なら宣言と実装を直接結び付けられます。リリースドキュメントでは、もう一つの方向もサポートされています。@c @implementationを使うと、インポートしたCヘッダーですでに宣言されている関数を実装でき、Swiftがシグネチャの一致を検証します。既存のC APIをインターフェイスの契約として維持する必要がある場合に便利です。Swift 6.3 Released
周囲のプロジェクトがすでにC++経由でSwiftを呼び出している場合は、Swiftが生成するC++バインディングを利用する方法もあります。公開されたSwift関数は、モジュール名前空間内のインラインC++関数として表現されます。Cの呼び出し側から使うには、別途C++としてコンパイルするアダプターを用意し、C向けの小さな宣言を公開する必要があります。C翻訳単位では、そのアダプターのCヘッダーをインクルードします。SwiftとC++の混在
このようなアダプターでは、extern "C"をC++の宣言または定義に指定すると、C言語リンケージが指定されます。ただし、C++ヘッダーの内容をC構文に変換するものではありません。C++ワーキングドラフトでは、リンケージ指定について説明し、言語リンケージと通常の宣言を区別しています。C++ワーキングドラフト: リンケージ指定
このチュートリアルでは、@cをサポートする新しいコンパイラにアップグレードするのが、直接的でサポート対象の方法です。この提案では、@_cdeclは実験的な前身として説明されています。新しい例で古い表記を使うと、重要なバージョンの違いが見えなくなります。C互換の関数と列挙型
問題が起きた境界を特定する
最初に失敗したビルド手順を手掛かりに、原因を絞り込んでください。ヘッダー生成の失敗、Cのパーサーエラー、シンボル未解決には、それぞれ別の修正が必要です。
| 最初に失敗した手順 | 次に確認すること |
|---|---|
Swiftが@cを拒否する |
選択中のSwiftコンパイラのバージョンを再確認してください。この例にはSwift 6.3以降が必要です。 |
CがPercent-Swift.hを見つけられない |
Swiftのコマンドが成功したことと、両方のファイルを意図したディレクトリからビルドしていることを確認してください。 |
Cがdemo_clamp_percentを見つけられない |
新たに生成されたヘッダーと属性の表記を確認し、ソースを変更した場合は再ビルドしてください。 |
| CがC++またはObjective-Cの構文を報告する | どのヘッダーをインクルードしたか、そのヘッダーをどのツールチェーンが生成したか確認してください。調査中も、Cとして明示的にコンパイルする設定を維持してください。 |
| リンク時に公開関数を解決できない | 最終コマンドにPercent.oが含まれていることと、現在のSwiftソースから再ビルドされていることを確認してください。 |
| 実行可能ファイルが予期しない結果を報告する | 表示された入力値と結果の組を読み、Swiftの分岐と照合してください。 |
この手順は、Clangが説明するコンパイラの各段階に沿っています。解析でソースの妥当性を確認し、オブジェクト生成でコンパイル済みコードを作り、リンクでコンパイル済みの入力を結合します。したがって、Cのコンパイルに成功しただけでは、課題の一部しか確認できません。Clangコマンドガイド
この例を応用する際は、C側に公開する関数を小さく保ち、新しい各パラメーターに明確な契約を定めてください。テキストならエンコーディングと長さの扱いを決め、バッファーならラッパーを書く前に容量と所有権を決めます。これらは、整数だけを扱うこの例の範囲を超えるインターフェイス設計上の決定です。
完成したローカルでの確認では、3つの結果を目視できます。生成されたヘッダーを明示的なCコンパイルで読み込めること、両方の実装を1つの実行可能ファイルにリンクできること、そして呼び出し側が選んだ入力を検証できることです。サンプルの計算処理を独自のSwift関数に置き換える際も、これらの確認手順を残してください。
出典
- Swift 6.3 Released — Swift.org
- C互換の関数と列挙型 — Swift Evolution
- コマンドラインツールをインストールする — Apple
- コマンドラインツールの設定を構成する — Apple
- コマンドラインからのビルドに関するXcode FAQ — Apple
- Swiftコンパイラドライバーのオプション — Swift Project
- Clangコマンドガイド — LLVM Project
- コンパイラのデバッグ — Swift Project
- SwiftとC++の混在 — Swift.org
- C++ワーキングドラフト: リンケージ指定 — C++ Working Draft