在Swift中包装C库(第1部分)

这篇文章是在Swift中包装C库的多部分指南中的第一篇。 第1部分将逐步完成构建Swift项目的过程,该项目可以使用Swift Package Manager(SPM)与C库libgraphqlparser进行交互。 其他部分将介绍如何将C接口包装为使用起来更自然的Swift API。

在探索特定库的示例时,此处描述的相同技术可以应用于大多数其他C库。

自2016年以来,在Shopify,我们一直在移动应用程序中使用GraphQL。GraphQL提供的优于典型REST API的优势之一是它具有定义明确的架构。

可以利用GraphQL模式编写代表各种查询和变异的网络响应的强类型Swift代码。 这段代码编写起来很繁琐且容易出错,因此我们决定构建一个可自动生成代表您的GraphQL查询和变异的Swift模型的工具。

构建这样的工具的首要挑战之一是找到一种方法来解析GraphQL语法中定义的查询和变异,并将其转换为可由Swift代码理解的抽象语法树。

构建语法分析器并非易事。 幸运的是,GraphQL组织已经发布了一个用C ++编写的开源解析器。 Swift无法直接与C ++代码互操作,但是libgraphqlparser项目提供了一个纯C API,只需做一点工作,就可以在Swift中使用它。

libgraphqlparser库的标头中定义了一些功能,可以完全满足我们的需求。

从libgraphqlparser的C头文件

第一个函数将采用GraphQL查询字符串,并将其转换为查询的AST表示形式。 对于本教程,我们将仅使用graphql_ast_to_json(ast)将AST转换为JSON并打印结果。

设置基本软件包

SPM要求定义一个包装系统库的Swift软件包,但是由于没有Swift代码,因此无法直接构建该软件包。 为了验证该库可以导入到Swift代码中,基本包必须将包装器包作为依赖项导入。 让我们构建该基本软件包。

  $ mkdir GraphQLParser 
$ cd GraphQLParser
GraphQLParser $ swift软件包init --type可执行文件

安装库

安装libgraphqlparser可以通过从源代码构建或使用以下命令通过自制程序来完成。

  $ brew安装libgraphqlparser 

完成初始项目设置后,我们需要将代码公开给Swift。 由于这是针对macOS命令行应用程序的,因此我们可以使用Swift Package Manager(SPM)。

为了向Swift代码公开系统库,我们需要告诉编译器在哪里可以找到我们要使用的代码。 通常,这包括将动态库的位置和任何必需的头文件的位置通知编译器。

在这种情况下,动态库可能已安装到/usr/local/lib/libgraphqlparser.dylib ,并且头文件将已写入/usr/local/include/graphqlparser

SPM通过要求用户定义一个包装系统库的程序包并为该库提供模块映射来解决此问题。 约定是在这些程序包之前使用大写的“ C”作为前缀,因此该程序包将被称为“ Clibgraphqlparser”。

在与GraphQLParser文件夹相邻的目录中初始化此软件包。

  GraphQLParser $ cd .. 
$ mkdir Clibgraphqlparser
$ cd Clibgraphqlparser
Clibgraphqlparser $ swift软件包init --type系统模块

定义模块图

SPM将自动生成一个模块图,该图描述现有标头的集合如何映射到模块的结构。 模块映射允许使用简单的import Clibgraphqlparser语句将代码导入Swift。

修改生成的模块映射的内容,使其具有以下内容。

Clibgraphqlparser的模块映射

让我们逐行介绍一下。

第一行将名为“ Clibgraphqlparser”的模块声明为系统模块。 根据Clang关于模块的文档, [system]属性指示“模块的所有标头都将被视为系统标头,从而抑制了警告”。

下一行声明指定的标头与此模块关联。 您可以在模块映射中具有多个标头声明,但是为了清楚起见,定义了一个自定义标头文件,该文件导入了该库的所有其他标头。

链接声明指定应链接的库的名称。 在这种情况下,我们要链接“ graphqlparser”。

最后, export *声明指定由该模块的标头导入的任何模块都应自动与其一起重新导出,并作为其API的一部分公开。

模块映射可能会使非C开发人员(如我)感到困惑,但是在大多数情况下,它们都将遵循相同的结构。

导入标题

并非所有软件包都需要执行此步骤,但为方便起见,请定义一个头文件,该文件导入libgraphqlparser的所有头。 将此文件Clibgraphqlparser.h并将其放置在Clibgraphqlparser目录的根目录中。 我们要导入的标头位于/usr/local/include/graphqlparserc/子目录中。 我们的自定义标头将包含以下导入语句。

Clibgraphqlparser.h

包装说明

现在我们有了模块映射和自定义头文件,我们需要提供软件包描述。 打开Package.swift并将内容更改为以下内容。

Clibgraphqlparser的Package.swift

让我们逐行查看包变量的声明。

第一行提供此程序包的名称Clibgraphqlparser。

接下来是pkgConfig条目。 通常(但并非总是)在计算机上安装系统库时,它将包含pkgConfig,该文件描述了SPM在何处可以找到动态库文件和该库的头文件。 您可以通过运行$ pkg-config --list-all来检查是否为您的库安装了$ pkg-config --list-all 。 如果未为您的库安装pkg-config,则必须显式链接库并包括头文件。 这将在下面讨论。

接下来,我们定义一个产品,名称为Clibgraphqlparser,带有一个Clibgraphqlparser的小工具。 下面定义的目标将具有类似的格式。 这种模式与其他非系统Swift包相同,因此这里没有新的解释。

这应该是该包装的所有必需品。 提交给母版,并为其提供版本标签。

  Clibgraphqlparser $ git初始化 
Clibgraphqlparser $ git add。
Clibgraphqlparser $ git commit -m“初始提交”
Clibgraphqlparser $ git标签1.0.0

现在,描述此库的软件包已包含所有必要的部分。 现在是构建它的时候了。 Clibgraphqlparser本身没有任何Swift代码,因此在其中运行swift run会引发错误。 正确构建它的方法是构建我们的GraphQLParser软件包,其中包含Clibgraphqlparser作为依赖项。

更新GraphQLParser的Package.swift以指向最新版本的Clibgraphqlparser。

GraphQLParser的Package.swift

打开GraphQLParser的main.swift并为Clibgraphqlparser添加一个导入语句。

main.swift的初步实现

现在我们可以在GraphQLParser目录中运行swift run ,它应该正确地获取并构建Clibgraphqlparser包。 根据您安装libgraphqlparser的方式,此命令可能会失败,并显示诸如“无法构建Objective-C模块Clibgraphqlparser”和“找不到’c / GraphQLAstNode.h’文件”之类的错误。 这些错误表明SPM无法找到要链接的头文件或动态库。 这通常是由于未安装libgraphqlparser的pkg-config导致的。

为了解决这个问题,我们显式链接了libgraphqlparser.dylib的存储目录,并包含包含其标题的目录。

  GraphQLParser $快速运行-Xcc -I / usr / local / include / graphqlparser -Xlinker -L / usr / local / lib 

这应该可以解决与libgraphqlparser相关的所有编译错误。 如果仍然收到构建错误和关于链接“由旧版Swift语言编译的库”的警告,则需要添加一个附加的链接器标志,以确保您链接到与活动工具链捆绑在一起的Swift标准库。 修改后的构建命令应如下所示。

  GraphQLParser $快速运行-Xcc -I / usr / local / include / graphqlparser -Xlinker -L $(xcode-select -p)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx   -Xlinker -L / usr / local / lib 

现在,您可以在Swift代码中调用libgraphqlparser的任何公共函数。 让我们尝试实现上述用例,以将查询字符串解析为其AST表示形式,然后将该AST转换为JSON字符串。 将GraphQLParser包中的main.swift内容更新为以下内容。

主Swift

运行此命令将打印示例查询的AST的JSON表示形式。

我们完成了! 还是……是吗? 我们已经使用Swift成功地链接和执行了C库中的代码,但是代码很丑陋。 使用OpaquePointer代替类型对象,并使用UnsafePointer代替Swift字符串可能容易出错,并且处理起来很麻烦。 手动释放内存也是一个细节,最好将其封装为实现细节。

我们已经成功地将C代码公开给Swift并调用了它的函数。 在本文的第2部分中,我们将探讨如何将此API包装到更强类型和Swift友好的内容中。

谢谢阅读! 如有任何疑问,可以在Twitter上找到我。

  • 文章源代码
  • SPM文档
  • lang模块文档
  • libgraphqlparser源代码
  • Clibgraphqlparser源代码