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 排版

相关文章
相关标签/搜索