dot Redis 8 来了——而且它是开源的

了解更多

如何用 Zig 编写 Redis 模块

更新 (2019 年 5 月 9 日): 已修改代码以适配 Zig 0.4.0 版本的新发布

几乎所有人的技术栈中都有一个 Redis 实例,或者至少了解 Redis。然而,并非所有人都知道 Redis 支持模块:自定义扩展,可以为 Redis 数据库添加新命令、数据类型和功能。

需要新的数据结构?全文搜索?想在 Redis 中存储图数据?

所有这些都可以通过 现有模块 实现,这些模块正由 Redis 社区积极开发。

如果您有尚未涵盖的特定用例,并且需要 Redis 提供的高性能,您可能有兴趣学习如何编写模块。

构建模块的默认方式是使用 C 语言并将其编译为动态共享库。

如果您了解 C 语言,那就没问题,但如果您不了解,从头设置一个 C 项目可能会感到有些不知所措。很容易忘记编译过程中的一个小细节,最终得到一个会无故出现段错误的二进制文件。

当您需要处理跨平台编译时,情况可能会更加令人沮丧,例如当您在 macOS 或 Windows 上进行开发,并打算部署到 Linux 时。

介绍 Zig

ZigAndrew Kelley 开发的一种全新的语言,它非常注重为 C 语言的编程方式带来现代化的便利。其结果是一种允许您构建完全兼容 C ABI 的二进制文件,同时能够访问诸如编译器提供的高级错误检查、更好的元编程能力、泛型、可选类型、错误类型等语言特性的语言。

模块如何与 Redis 交互

Redis 模块是对象文件,可以在运行时由 Redis 动态加载。模块期望访问 Redis 暴露的一些函数,这些函数允许它在 Redis 生态系统中运行。模块唯一需要实现的接口是一个名为 RedisModule_OnLoad() 的函数,通常用于在 Redis 中注册模块提供的所有新命令。

这意味着您可以使用任何可以编译为 C 兼容动态库的语言,而 Zig 使其变得尤为简单,同时允许您在私有代码中使用更现代的抽象。

让我们编写一个非常简单的模块,名为 testmodule,它实现了一个 test.hello 命令,该命令在被调用时仅向客户端发送“Hello World!”。

编写模块 – C 方式

在 C 语言中,编写 Redis 模块最直接的方法是从 Redis 官方仓库(unstable 分支) 下载一份 redismodule.h 头文件,将其包含在代码开头,然后编写模块。

头文件是 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 方式

在我们开始用 Zig 编写之前,您可能想知道,如果您不是用 C 语言编写,如何导入 C 头文件中的所有定义?

这似乎已经是一个很大的障碍,但 Zig 将与 C 项目集成放在首位,因此它提供了两种快速导入头文件的方式。

最快的方法是直接调用 @cInclude(),它可以直接导入头文件;第二种方法是使用编译器命令将 C 代码翻译成 Zig(@cInclude() 在底层就是这样做的)。

虽然直接,但在我们的例子中,第一种选择并非最佳,原因有几个,最重要的是 Zig 是一种安全语言,自动翻译头文件会导致 Zig 假定每个指针都可能为 null,因此在进行任何操作之前需要用户进行显式检查。

Zig 中的 Null 值

许多人认为,null 指针是计算机科学中发生的最糟糕的事情之一

在 Zig 中,可以为 null 的值被包装在一个名为 Optional 的通用容器类型中。

Optional 类型变得越来越普遍:仅举几例,SwiftKotlinRust 都拥有它们。

当您拥有一个可选值(无论是指针还是其他任何值)时,在访问它之前,您必须对其进行解包。这强制对任何潜在的 null 值进行显式检查,而无需进行不加区分的检查或冒未处理异常的风险。

不幸的是,null 在 C 语言中对于指针是有效值,因此无法指定一个指针是否允许为 null。这意味着从头文件导入函数签名时,每个作为指针的形参都将被翻译成 Zig 的可选指针,这是正确且安全的选择,但如果您确定某个指针永远不会为 null,则会使使用导入的符号变得不必要地冗长。

为了清楚地说明,这是直接导入头文件时 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;
}

确保我们不会尝试解引用 null 指针非常好,但是如果我们知道一个指针永远不会为 null(例如 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`,可以轻松生成 C 风格字符串,正如您在我们的 Zig 代码中看到的。

这个简单的例子表明,C 代码可以非常直接地翻译成 Zig,但不要以为您最终得到的只是“加了额外步骤的 C”。当您开始编写一个真正处理数据的模块时,有很多特性会让您深感赞赏。

有关功能的完整列表,请参阅官方 Zig 文档

构建 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 发送 Pull Request,以便将其添加到列表中。

编码愉快!