继上文如何在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 中的指针使用