更新(2019 年 5 月 9 日):按照 Zig 版本 0.4.0 的新版本更改了代码
每个人堆栈的某处都会有一个 Redis 实例或者至少知道 Redis。但是并不是每个人都了解 Redis 支持模块:添加新指令、数据类型和功能到 Redis 数据库的定制扩展。
需要一个新数据结构?全文搜索?想将图形数据存储在 Redis 中?
有了 Redis 社区积极开发的现有模块,所有这些已经成为可能。
如果您有一个尚未涵盖的具体使用案例并且需要 Redis 提供的性能,您可能会对学习如何编写模块感兴趣。
构建模块的默认方式是使用 C 语言并以共享动态库的形式编译。
如果您了解 C 语言,那么一切都好,但如果不了解,设置全新的 C 项目会让人感到难以招架。很容易忘记编译过程中的一个小细节,然后得到一个不带任何明显原因的二进制文件 segfaults。
当您必须处理跨平台编译时会更加恼人,例如,当您在 macOS 或 Windows 上开发并打算部署在 Linux 上时。
Zig 是Andrew Kelley开发的一种全新语言,它非常重视将现代便利带入 C 语言的编程方式。它产生的语言允许您在访问编译器的高级错误检查、更好的元编程设施、泛型、可选项、错误类型等语言特性的同时,构建完全兼容 C ABI 的二进制文件。
Redis 模块是可以由 Redis 在运行时动态加载的对象文件。模块希望访问 Redis 公开的几个函数,这些函数允许模块在 Redis 生态系统中操作。模块需要实现的唯一接口是一个名为 RedisModule_OnLoad() 的函数,它通常用于在 Redis 中注册模块提供的所有新指令。
这意味着您可以使用任何可编译为兼容 C 语言的动态库的语言,而 Zig 使得这件事变得特别容易,并且允许您在您的私有代码中使用更现代的抽象。
让我们编写一个名为 testmodule 的非常简单的模块,它实现一个 test.hello 指令,该指令仅在调用时向客户端发送 “Hello World!”。
在 C 语言中,编写 Redis 模块的最直接方法是从 Redis 官方存储库(不稳定分支) 下载 redismodule.h 头文件的副本,在您的代码开头 include 它并编写模块。
头文件是 C 描述另一段代码的接口的一种方法,这种代码并不会在编译阶段出现,从而令编译器知道是否有未实现符号未被正确使用。
以下是我们的模块代码在 C 中的样子
#include "redismodule.h"
int HelloWorld_Command(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
RedisModule_ReplyWithSimpleString(ctx, "Hello World!");
return REDISMODULE_OK;
}
int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
if (RedisModule_Init(ctx,"testmodule",1,REDISMODULE_APIVER_1)
== REDISMODULE_ERR){
return REDISMODULE_ERR;
}
if (RedisModule_CreateCommand(ctx,"test.hello", HelloWorld_Command,
"readonly", 0, 0, 0) == REDISMODULE_ERR){
return REDISMODULE_ERR;
}
return REDISMODULE_OK;
}
除了头文件导入,脚本仅包含两个函数实现:HelloWorld_Command() 和 RedisModule_OnLoad()。
如前所述,RedisModule_OnLoad() 由 Redis 在加载模块时调用,而 HelloWorld_Command() 是我们的示例命令
现在,我们已经用 C 编写了一个简单的 HelloWorld 模块,我们来看看如何使用 Zig 编写该模块。
在开始使用 Zig 编写代码之前,你可能会想,如果你没有用 C 来编写代码,该如何导入 C 头文件的所有定义?
这看起来确实是一个巨大的障碍,但 Zig 将与 C 项目的集成作为一项优先任务,因此它提供了两种快速导入头文件的方法。
最快的办法就是直接调用 @cInclude(),它允许直接导入头文件,而第二种方法是使用编译器命令将 C 代码翻译成 Zig(这是 @cInclude() 在后台执行的操作)。
虽然第一种方案很直接,但在我们的案例中由于以下几个原因,这不是一个最优方案,其中最重要的原因是 Zig 是一种安全语言,头文件的自动翻译导致 Zig 假设所有指针都可能为 null,从而要求用户在执行任何操作之前必须进行明确的检查。
据许多人认为,null 指针是计算机科学中最糟糕的事情之一。
在 Zig 中,可以为 null 的值包装在称为 Optionals 的泛型容器类型中。
可选类型正在变得非常普遍:Swift、Kotlin、Rust 中都有,仅举几例。
当你有可选值(无论是指针还是其他任何东西)时,在访问它之前,你必须展开它。这样可以对任何潜在的 null 值进行强制性显式检查,而无需不加区别地进行检查或冒引发未处理异常的风险。
不幸的是,null 是 C 中指针的一个合法值,由于这个原因,无法指定一个指针是否被允许为空。这意味着,从头文件导入函数签名时,每个形式参数(它是一个指针)将被转换为 Zig 可选指针,这是正确且安全的选择,但是如果你确定某个给定指针永远不会为空,那么使用导入的符号将不必要地冗长。
为了明确地向你展示,这是直接导入头文件时的 HelloWorld_Command() 的样子
export fn HelloWorld_Command(ctx: ?*redis.RedisModuleCtx, argv: ?[*]?*redis.RedisModuleString, argc: c_int) c_int {
_ = redis.RedisModule_ReplyWithSimpleString.?(ctx, c"Hello World!");
return redis.REDISMODULE_OK;
}
确保我们不会尝试取消引用空指针是非常好的,但是如果我们知道一个指针永远不会为空,就像 Redis 命令的形式参数一样,这会有点冗长。
解决方案是使用
$ zig translate-c redismodule.h
这将获取等效于头文件的 Zig 文件,并更改一些类型签名,以节省一些不必要的可选项拆包。
为了更方便,你可以 下载 redismodule.zig 的经过清理的副本(兼容 Zig 0.4.0 和 Redis 5.0)。只要知道它不受官方支持,并且如果出现问题,你可能不得不亲自动手。
然后,编写在 Zig 中时的模块看起来像这样
const redis = @import("./redismodule.zig");
export fn HelloWorld_Command(ctx: *redis.RedisModuleCtx, argv: [*c]*redis.RedisModuleString, argc: c_int) c_int {
_ = redis.RedisModule_ReplyWithSimpleString(ctx, c"Hello World!");
return redis.REDISMODULE_OK;
}
export fn RedisModule_OnLoad(ctx: *redis.RedisModuleCtx, argv: [*c]*redis.RedisModuleString, argc: c_int) c_int {
if (redis.RedisModule_Init(ctx, c"testmodule", 1, redis.REDISMODULE_APIVER_1)
== redis.REDISMODULE_ERR) {
return redis.REDISMODULE_ERR;
}
if (redis.RedisModule_CreateCommand(ctx, c"test.hello", HelloWorld_Command,
c"readonly", 0, 0, 0) == redis.REDISMODULE_ERR) {
return redis.REDISMODULE_ERR;
}
return redis.REDISMODULE_OK;
}
如你所见,转换非常简单,主要区别在于导入的符号存储在 redis 常量中。
值得注意的另一件事是,由于字符串在内存中表示方式的不一致,C 经常会与其他语言进行不稳定的互操作。在 C 中,字符串是指向以 null 结尾的字节数组的指针,而其他语言则有各种不同的表示形式。Zig 本身不使用 C 式字符串,但是通过在字符串文字前加上 `c`,它可以轻松生成它们,如你可以在我们的 Zig 代码中看到的那样。
这个简单的例子表明,C 代码大部分可以直接翻译成 Zig,但不要认为它会变成“带有额外步骤的 C”。当你开始编写一个真正的移动数据的模块时,你肯定有很多会赞赏的功能。
查阅 Zig 官方文档,了解完整的功能列表。
Zig 有一个专门用于共享库的构建命令
$ zig build-lib -dynamic module.zig
如果你需要为 64 位 Linux 进行交叉编译
$ zig build-lib -dynamic module.zig –target-os linux –target-arch x86_64
要试用你的模块,你可以在 redis-cli 中或者通过 Redis Enterprise GUI 使用以下命令
> MODULE LOAD /path/to/module.so.0(或者 macOS 上的 libmodule.0.0.0.dylib)
OK
> test.hello
Hello World!
我们了解了 Zig 如何让集成 C 生态系统变得相对轻松。您可以使用 @cInclude() 和 c”Hello World” 等便捷的快捷方式以及 translate-c 编译器命令等非常实用的转义舱,以在需要更多控件的时候进行控制。
最终获得的语言可以提供更多安全保障,并提供极其简单的构建过程,甚至支持单命令交叉编译。
现在只需了解,您可以在 Redis 模块中实际执行哪些操作。
官方网站上包含已发布模块列表和完整模块 API 参考。
如果您想分享自己编写的 Redis 模块,并认为该模块社区会感兴趣,请将一个拉取请求发送至 antirez/redis-doc 以将其添加到列表中。
祝您玩得开心!