如何在Swift中调用C库(进阶篇)

继上文如何在Swift中调用C库(入门篇),这次我要稍微深入的讲解一下Swift封装C库遇到的问题:
  • C语言中的Variadic function在Swift中不可用。
  • C语言映射到Swift中指针转换。

Variadic function is unavailable

如果在上文中提到的SwiftRedis应用里继续调用Hiredis的方法,比如根据Hiredis官方的example,利用redisCommand方法来向redis发送命令:
let setReply:UnsafeMutablePointer<Void> = redisCommand(conn, "SET foo 1234")
freeReplyObject(setReply)
那么编译的时候将会出现以下结果:
error: 'redisCommand' is unavailable: Variadic function is unavailable
是的,Swift虽然能兼容大量C语言语法,但是对于Variadic function和通过Macro定义的方法是无法导入的。可是C语言这些灵活的特性又是经常被很多C库使用,所以在使用Swift封装C库的时候就会频频遇到这样的问题。遇到这样的问题,我们可以通过建立一个C库来桥接原始库,重写这些方法。

创建桥接库

我们创建一个C库,名为:hiredis-bridge,里面包含hiredis-bridge.h、hiredis-bridge.c和Makefile三个文件。我们只需要在这个C库中重新创建一个redisSendCommand方法来封装redisCommand就可以解决问题了。这里我们只使用到一些简单的C语言语法就行了。 hiredis-bridge.h的内容为:
#ifndef hiredis_bridge_h
#define hiredis_bridge_h

#include "hiredis/hiredis.h"

void *redisSendCommand(redisContext *c, const char *format);

#endif
hiredis-bridge.c内容为:
#include "hiredis-bridge.h"
#include "hiredis/hiredis.h"

void *redisSendCommand(redisContext *c, const char *format){
    return redisCommand(c, format);
}
创建一个Makefile来帮助我们编译这个C库:
TARGET = hiredis_bridge
LIB_NAME = hiredis_bridge

PREFIX ?= /usr/local

all: $(TARGET)

$(TARGET): *.c
    clang -c *.c
    ar -rcs lib$(LIB_NAME).a *.o
    rm *.o

install:
    mkdir -p $(TARGET)/usr/local/lib
    mkdir -p $(TARGET)/usr/local/include/$(TARGET)
    cp *.h $(TARGET)/usr/local/include/$(TARGET)
    cp lib$(LIB_NAME).a $(TARGET)/usr/local/lib/
    mkdir -p $(PREFIX)
    cp -r $(TARGET)/usr/local/* $(PREFIX)/
    rm -r $(TARGET)
以上,这个简单的C库就写好了,以上代码托管在这里:https://github.com/fengluo/hiredis-bridge/tree/f5e02dc1f6a624d73bcd92d7e9860d8d05d3579b tag: 0.1.0 在Linux下输入以下命令编译安装:
make
sudo make install

改进CHiredis

是的,既然我们创建了一个桥接库来解决问题,所以我们改进一下之前的映射库CHiredis,把桥接库整合进去。 修改CHiredis的module.modulemap:
module CHiredis [system] {
    header "/usr/include/hiredis/hiredis.h"
    header "/usr/local/include/hiredis_bridge/hiredis-bridge.h"
    link "hiredis"
    link "hiredis_bridge"
    export *
}
然后使用git重新提交该文件:
git add module.modulemap
git commit -m 'add hiredis_bridge'
git tag 0.2.0
以上代码已经更新到github:https://github.com/fengluo/CHiredis/tree/6a7d3f154513ef83a3f775585a3e3f0a53d6b4ee,tag:0.2.0

改进SwiftHiredis,处理Swift中的指针

重新回到我们的SwiftHiredis,这个时候我们就可以使用全新的CHiredis来发送redis命令了。首先修改一下Package.swift:
import PackageDescription

let package = Package(
    dependencies: [
        .Package(url: "https://github.com/fengluo/CHiredis", majorVersion: 0, minor: 2)]
)
这里我直接依赖了我的github上的CHiredis。 然后在main.swift中修改代码为:
import CHiredis

let conn:UnsafeMutablePointer<redisContext>  = redisConnect("127.0.0.1",6379)

let setReply:UnsafeMutablePointer<Void> = redisSendCommand(conn, "SET foo 1234")
freeReplyObject(setReply)

let getReply:UnsafeMutablePointer<Void> = redisSendCommand(conn, "GET foo")

var getReplyPtr:UnsafeMutablePointer<redisReply> = unsafeBitCast(getReply, UnsafeMutablePointer<redisReply>.self)
var str:String = String.fromCString(getReplyPtr.memory.str)!
print(str)

freeReplyObject(getReply)
redisFree(conn)
这里的最复杂的地方是C语言中的类型到Swift中类型的映射,尤其是指针的使用。可怜我早把C语言的指针还给谭浩强了,这个时候再捡起来还真是颇为辛苦,掉了许多坑才让代码正常运行。 简单解释一下,Apple官方文档已经对C到Swift的指针转换做了阐述,其中最基本的指针转换:
C Syntax Swift Syntax
const Type * UnsafePointer<Type>
Type * UnsafeMutablePointer<Type>
很多C库的返回值是void *,不用强制指定类型,是一种非常灵活的指针用法。所以对应到Swift中为UnsafePointer。不过对于指向struct的指针来说,在Swift中访问其对象,需要用unsafeBitCast转换到一个具体类型上。所以就有了这样的用法:
var getReplyPtr:UnsafeMutablePointer<redisReply> = unsafeBitCast(getReply, UnsafeMutablePointer<redisReply>.self)
其他不多作解释了,之后我再写文章详解swift中指针的运用。 编译并运行:
swift build
.build/debug/SwiftHiredis
就会得到我们在代码中给foo设置的值1234。 以上代码已经更新到github:https://github.com/fengluo/SwiftHiredis/tree/02e7f86ba1db000217f0a8ce5f932065c250934d,tag:0.2.0

总结

至此,我们已经解决了主要的问题,能够使用swift去操作redis。我要说的是,封装Hiredis、开发redis的Swift客户端会有更好的思路,不一定需要像上面那样绕弯,有很多办法可以简化上面的操作。我这里只是拿Hiredis举个例子来演示如何解决调用C库时可能会遇到的问题。 下篇文章是完结篇,我会完善这个示例,使其可以同时兼容Mac平台。

参考文章

Wrapping variadic functions for use in Swift Using Swift with Cocoa and Objective-C (Swift 2.1) Using Legacy C APIs with Swift Swift 中的指针使用