使用 Swift 6.3 或更新版本,為全域函式標記 @c、產生相容性標頭、將呼叫端編譯為 C,並使用 swiftc 連結。本指南會建置一個小型原生 Mac 命令列程式,示範每個界面邊界。

讀者要完成的工作很明確:從一般的 .c 檔案呼叫 Swift 計算函式,而不將該檔案編譯為 Objective-C 或 C++。Swift 官方版本說明指出,@c 是 Swift 6.3 引入、受支援的 C 互通功能。請先檢查已安裝的編譯器,再參考舊版實驗性屬性的建議。Swift 6.3 Released

什麼條件能讓 C 呼叫 Swift 函式?

三個部分必須一致:C 編譯器讀取的宣告、實作使用的呼叫慣例,以及提供給連結器的物件檔。標頭檔負責第一部分;它無法提供缺少的實作,也無法改變既有函式的 ABI。

@c 屬性會讓全域 Swift 函式使用 C 呼叫慣例,並限制其簽章只能使用可由 C 表示的型別。可選的屬性引數可指定其 C 名稱。Swift 會在產生的相容性標頭中,於 C 相容區段輸出對應的宣告。這些行為載明於已實作的 Swift Evolution 提案中。C 相容函式與列舉

在這個範例中,介面接受並傳回 C 整數。範例計算會將顯示百分比限制在 0 到 100 的含端點範圍內。無須協調緩衝區、已配置的結果或物件生命週期,因此初次建置可專注於編譯與連結。

先檢查 Mac 工具鏈

你需要 macOS 開發環境,並具備 Clang、macOS SDK,以及 Swift 6.3 或更新版本。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 選取作用中的版本,或在 shell 工作階段中使用 DEVELOPER_DIR 覆寫。設定命令列工具

以下指令使用 xcrun 叫用所選的開發工具。Apple 在命令列建置參考文件中說明了此工具的查找功能。使用 Xcode 從命令列建置:常見問題

建立一個空白工作目錄,並將下方兩個原始碼檔案存入其中。請在該目錄執行建置指令。這是為本機 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;
}

三個輸入值分別測試下限、保持不變的數值,以及上限。程式會明確檢查結果,並在不相符時傳回非零狀態,因此比只印出數字的示範更實用。

產生標頭檔、編譯 C,並連結程式

在包含這兩個檔案的目錄中,於 shell 執行以下步驟。任何步驟失敗都會中止後續指令:

(
    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 相容函式與列舉

第二個指令是關鍵的原始碼語言檢查。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 的函式保持精簡,並為每個新增參數明確訂定契約。若處理文字,請決定編碼與長度處理方式;若處理緩衝區,請在撰寫包裝程式前決定容量與所有權。這些介面設計決策超出本整數範例的範圍。

完成本機檢查後,會有三項可觀察結果:明確以 C 編譯時可接受產生的標頭檔、兩個實作可連結為同一個執行檔,以及呼叫端會驗證選定的輸入值。將範例計算替換成自己的 Swift 函式時,請保留這些檢查。

來源