Swift 調用 C 的正確姿式

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

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

Bridging Header

當咱們在一個 Swift 項目中添加 C 源文件時,Xcode 會詢問是否添加 Objective-C 橋接頭文件,這跟咱們在 Swift 項目中添加 OC 文件同樣。接着咱們只須要在 Bridging Header 中添加須要暴露給 Swift 代碼的頭文件:github

#include "test.h"
複製代碼

test.h 中聲明瞭一個 hello 函數:web

#ifndef test_h #define test_h  #include <stdio.h>  void hello(void);  #endif /* test_h */ 複製代碼

而後在 tesh.c 中實現了它:swift

#include "test.h"
 void hello() {  printf("Hello World"); }  複製代碼

如今咱們就能夠在 Swift 代碼中調用 hello() 了。xcode

Swift Package Manager

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

下面咱們將用 Swift 封裝一個易用的 OpenGL 程序庫。經過這個例子,咱們基本上能夠掌握如何在一個 Swif 庫中與 C 進行交互了。app

設置 SPM

爲導入 C 程序庫設置一個 Swift 包管理器項目並非什麼難事,不過仍是有很多的步驟要完成。編輯器

如今讓咱們開始建立一個新的 SPM 項目吧。切換要保存代碼的目錄,執行下面的命令建立一個 SPM 包:ide

$ 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: ["OpenGLApp"])  ],  dependencies: [],  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 排版

相關文章
相關標籤/搜索