1. 程式人生 > Android開發 >Swift 呼叫 C 的正確姿勢

Swift 呼叫 C 的正確姿勢

自從筆者第一次嘗試 Swift 到現在已經過去 5 年多了,從Swift 的第一個版本到現在的 Swift 5.2,Swift 語言發生了天翻地覆的變化。 Swift 生態也已經很完善,日常開發中用到的各種庫基本都支援了 Swift。那些現在還在糾結要不要使用 Swift 的同學可以看看這篇文章 ,文章中提到的幾個問題幾乎涵蓋了 OC 與 Swift 混編時會遇到的一些問題,文章中都給出了相應的解決方案。

Swift 和 Objective-C 以及 C、C++(Swift 不能直接呼叫 C++,必須通過 OC進行呼叫) 混編的阻力非常小。它可以自動橋接 objective-C 的型別,甚至可以橋接很多 C 的型別。這就可以讓我們在原有庫的基礎上,使用 Swift 開發出簡潔易用的 API。Swift 和 Objective-C 混編的文章不少,在這篇文章中,我們將學習如何讓 C 與 Swift 進行互動。

Bridging Header

當我們在一個 Swift 專案中新增 C 原始檔時,Xcode 會詢問是否新增 Objective-C 橋接標頭檔案,這跟我們在 Swift 專案中新增 OC 檔案一樣。接著我們只需要在 Bridging Header 中新增需要暴露給 Swift 程式碼的標頭檔案:

#include "test.h"
複製程式碼

test.h 中宣告瞭一個 hello 函式:

#ifndef test_h
#define test_h

#include <stdio.h>

void hello(void);

#endif /* test_h */
複製程式碼

然後在 tesh.c 中實現了它:

#include "test.h"

void hello() {
    printf("Hello World");
}

複製程式碼

現在我們就可以在 Swift 程式碼中呼叫 hello() 了。

Swift Package Manager

上面使用 Bridging header 的方式主要適用於 C 原始碼跟 Swift 程式碼處於同一個 App target 下,對於那些獨立的 Swift Framework 就不適用了,在這種情況下就需要使用 Swift 包管理器(Swift Package Manager,下文簡稱SPM)了。從 Swift 3.0 開始我們就可以使用 SPM 來構建 C 語言的目標 (target)了。

下面我們將用 Swift 封裝一個易用的 OpenGL 程式庫。通過這個例子,我們基本上可以掌握如何在一個 Swif 庫中與 C 進行互動了。

設定 SPM

為匯入 C 程式庫設定一個 Swift 包管理器專案並不是什麼難事,不過還是有不少的步驟要完成。

現在讓我們開始建立一個新的 SPM 專案吧。切換要儲存程式碼的目錄,執行下面的命令建立一個 SPM 包:

$ mkdir OpenGLApp
$ cd OpenGLApp
$ swift package init --type library
複製程式碼

我們通過 swift package init --type library 命令建立了一個名為 OpenGLApp 的 Swift 庫。我們可以開啟 Package.swift 檔案看看裡面的內容(刪除了無關內容):

// swift-tools-version:5.2
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "OpenGLAPP",   products: [
        .library(name: "OpenGLApp",targets: ["OpenGLApp"])
    ],    dependencies: [],    targets: [
        .target(
            name: "OpenGLApp",            dependencies: [],    ]
)
複製程式碼

為了完成一個可以執行的 OpenGL 程式,我們需要依賴 GLFWGLEW 這兩個 C 語言庫。GLFW 給我們提供了一個視窗和上下文用來渲染,這樣我們就不用去書寫作業系統相關程式碼了。GLEW 提供了用於確定其 OpenGL 擴充套件支援在目標平臺上高效的執行時間的機制。

將 C 程式庫匯出為模組

由於 GLFW 和 GLEW 都是由 C 編寫的庫,所以我們先要解決如何讓 Swift 找到這些 C 語言庫,這樣,才能在 Swift 呼叫它們。在 C 裡,可以通過 #include 一個或多個庫的標頭檔案的方式來訪問它們。但是 Swift 無法直接處理 C 的標頭檔案,它依賴的是模組 (Module)。為了讓一個用 C 和 Objective-C 編寫的庫對 Swift 編譯器可見,它們必須安照 Clang Module 的格式提供一份模組地圖 (Module map)。它的主要作用就是列出構成模組的標頭檔案。

因為 GLFW 和 GLEW 並沒有提供模組地圖,所以我們需要在 SPM 裡定義一個專門生成模組地圖的目標。它的作用就是把以上的 C 語言庫封裝成模組,這樣就可以在另一個 Swift 模組中呼叫它們了。

首先,我們需要安裝 glew 和 glfw,如果是 macOS 系統可以使用 homebrew 來安裝。其他的系統就使用相關的包管理器安裝就可以了。

接著開啟 Package.swift, 在 targets 中增加如下內容:

 ...
targets: [
    ....
    .systemLibrary(
        name: "Cglew",        pkgConfig: "glew",        providers: [
            .brew(["glew"])
        ]),    .systemLibrary(
        name: "Cglfw",        pkgConfig: "glfw3",        providers: [
            .brew(["glfw"])
        ]),]
複製程式碼

在上面的 Package.swift 中,我們新添加了兩個系統程式庫目標(system library target)。所謂的系統程式庫目標是指那些由系統級別的包管理器安裝的程式庫,例如我們使用 homebrew 安裝的一些程式庫。Sample 目標是最終的可執行程式,OpenGLApp 是我們將要使用 Swift 封裝的 OpenGL 庫,CglewCglfw 兩個系統程式目標就是我們製作的可以在 Swift 中呼叫的模組。

在系統程式庫目標中 pkConfigproviders 兩個引數需要說明一下:

  • providers 指令是可選的,在目標庫沒有被安裝時,它為 SPM 提供了用於安裝庫的方式的提示。
  • pkConfig 指定了pk g-config 檔案的名稱,Swift 包管理器可以通過它找到要匯入的庫的標頭檔案和庫搜尋路徑。pkConfig 的名稱我們可以在庫的安裝路徑的 lib/pkconfig/xxx.pc 中找到,以我電腦中安裝的 glew 為例,它的位置是 /usr/local/Cellar/glew/2.1.0/lib/pkgconfig/glew.pc,所以上面 pkConfig 中設定的就是 glew

接下來我們需要在 Sources 目錄下為系統程式庫目標建立一個儲存檔案的目錄,該目錄名稱必須跟上面 Package.swift 中定義的目標的 name 屬性一致。這裡我以 Cglfw 為例:

$ cd Sources && mkdir Cglfw
複製程式碼

在 Cglfw 目錄中新增一個 glfw.h 檔案,並新增如下內容:

#include <GLFW/glfw3.h>
複製程式碼

接著新增一個 module.modulemap 檔案,它應該是下面的樣子:

module Cglfw [system] {
    header "glfw.h"
    export *
}
複製程式碼

我們新增 glfw.h (名稱可以自己定義)檔案的目的是繞過模組地圖中必須包含絕對路徑的限制,否則的話,我們就必須在 modulemap 檔案中的 header 中指定 glfw3.h 標頭檔案的絕對路徑,在我的電腦上就是 /usr/local/Cellar/glfw/3.3.2/include/GLFW/glfw3.h,這樣就將 GLFW 的路徑硬編碼到模組地圖中了。使用了我們新增的 glfw.h 檔案,SPM 就會從 pkg-config 檔案中讀取正確的標頭檔案搜尋路徑,並將它新增到編譯器的呼叫中。

我們可以按照同樣的方式將 GLEW 匯出為模組,這裡我就不演示了。上面是將安裝在系統中的 C 程式庫匯出為模組,不過有些情況下我們只有 C 程式庫的原始碼,這個時候我們仍然可以使用 SPM 將 C 程式原始碼匯出為模組。

C 原始碼匯出為模組

將 C 原始碼匯出為模組也非常簡單,其實也是編寫模組地圖的過程,不過這個過程我們可以藉助 SPM 自動幫我們完成。

我們可以從這裡下載 GLEW 的原始碼。跟上面的步驟一樣,在 Sources 目錄下建立一個 Cglew 子目錄,並將解壓後的 GLEW 原始碼中 include 和 src 目錄拷貝到 Cglew 目錄下。然後我們在 Package.swift 中新增如下內容:

.target(name: "Cglew")
複製程式碼

在上面的過程中我們並沒有編寫模組地圖,並不是說通過這種方式不需要模組地圖,而是 SPM 自動幫我們完成的。我們將需要暴露給外部的標頭檔案放到 include 目錄下,編譯時 SPM 就會自動生成模組地圖。當然我們也可以通過 publicHeadersPath 引數來指定需要暴露給外部標頭檔案的路徑。

接著我們可以來完成 OpenGLApp 這個目標了。在 OpenGLApp 目錄中新增一個 GLApp.swift 檔案。現在,我們就可以在 Swift 檔案中使用 import Cglew,import Cglfw,並呼叫 GLFW 和 GLEW 中提供的 API 了。有一點不要忘記,我們需要在 Package.swift 檔案中 OpenGLApp 這個目標的 dependencies 新增我們都依賴:

.target(
    name: "OpenGLApp",    dependencies: ["Cglfw","Cglew"],    linkerSettings: [
        .linkedFramework("OpenGL")
    ]),複製程式碼

為了方便在 Xcode 中編寫並除錯程式,可以使用 swift package generate-xcodeproj 命令來生成一個 Xcode 工程。

在通過 import Cglew 引入 Cglew 模組並構建專案,你會發現 Xcode 報了大量錯,這個時候可以在 Cglew 目標中的 glew.h 檔案最上面新增 #define GLEW_NO_GLU

後面的主要工作就是編寫 OpenGL 程式碼了,這裡就不展開了,畢竟不是本文的重點。

接著我們可以新增一個用於執行該庫的可執行程式的目標。我們在 Sources 目錄下新增 Sample 子目錄,並新增一個 main.siwft 檔案,並在 Package.swift 中的 targets 新增一個 Sample 目標:

.target(
 name: "Sample",  dependencies: ["OpenGLApp"]),複製程式碼

我在 main.siwft 中呼叫了自己封裝的 OpenGLApp 的 Swift 庫:

import OpenGLApp

let app = GLApp(title: "Hello,OpenGL",width: 600,height: 600)
app.run()
複製程式碼

SPM 會將包含有 main.swift 檔案的目標作為可執行檔案目標。所以我們在用 SPM 開發庫時,庫檔案中不要有 main.swift 檔案,否則的話,SPM 會將該目標作為可執行檔案而不是一個庫,這樣就無法正確地和其他庫或可執行檔案進行連結了。

如果我們繼續在終端中執行 swift run 命令,這時 SPM 就會構建並執行這個應用程式(你可以從這裡 找到著色器的程式碼,這裡找到初始化頂點資料的程式碼)。

Hello,window
Hello,window

下面是完整的 Package.swift:

// swift-tools-version:5.2
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "GLAPP",    products: [
        .library(name: "OpenGLApp",    targets: [
        .target(
            name: "Sample",            dependencies: ["OpenGLApp"]),        .target(
            name: "OpenGLApp",            dependencies: ["Cglew","Cglfw"],            linkerSettings: [
                .linkedFramework("OpenGL")
            ]),        .systemLibrary(
            name: "Cglew",            pkgConfig: "glew",            providers: [
                .brew(["glew"])
            ]),        .systemLibrary(
            name: "Cglfw",            pkgConfig: "glfw3",            providers: [
                .brew(["glfw"])
            ]),    ]
)
複製程式碼

總結一下,要想讓 Swift 模組能呼叫 C 程式,只需要將 C 程式程式碼匯出為模組即可。而匯出模組只需要按照Clang Module 的格式提供一份模組地圖。

回顧

在 Swift 程式碼中使用 C 程式程式碼其實是一件很簡單的事情,比起用 Swift 重寫一個已經存在的 C 程式庫,為什麼不直接在 Swift 中使用它們呢。當然在實際使用過程中也肯定會遇到一些問題,比如 C 中的指標,回撥函式等等,不過這些並不是什麼大的問題,不知道如何使用只是表明我們對 Swift 某些地方還不熟悉。

本文使用 mdnice 排版