点 极速的未来即将来临,就在您所在的城市举办的活动中。

在 Redis 发布会上与我们共聚

在 Zig 中编写 Redis 模块的方法

更新(2019 年 5 月 9 日):按照 Zig 版本 0.4.0 的新版本更改了代码

每个人堆栈的某处都会有一个 Redis 实例或者至少知道 Redis。但是并不是每个人都了解 Redis 支持模块:添加新指令、数据类型和功能到 Redis 数据库的定制扩展。

需要一个新数据结构?全文搜索?想将图形数据存储在 Redis 中?

有了 Redis 社区积极开发的现有模块,所有这些已经成为可能。

如果您有一个尚未涵盖的具体使用案例并且需要 Redis 提供的性能,您可能会对学习如何编写模块感兴趣。

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

如果您了解 C 语言,那么一切都好,但如果不了解,设置全新的 C 项目会让人感到难以招架。很容易忘记编译过程中的一个小细节,然后得到一个不带任何明显原因的二进制文件 segfaults。

当您必须处理跨平台编译时会更加恼人,例如,当您在 macOS 或 Windows 上开发并打算部署在 Linux 上时。

Introducing Zig

Zig 是Andrew Kelley开发的一种全新语言,它非常重视将现代便利带入 C 语言的编程方式。它产生的语言允许您在访问编译器的高级错误检查、更好的元编程设施、泛型、可选项、错误类型等语言特性的同时,构建完全兼容 C ABI 的二进制文件。

模块如何与 Redis 交互

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

这意味着您可以使用任何可编译为兼容 C 语言的动态库的语言,而 Zig 使得这件事变得特别容易,并且允许您在您的私有代码中使用更现代的抽象。

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

编写模块 – C 语言的方法

在 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 方法

在开始使用 Zig 编写代码之前,你可能会想,如果你没有用 C 来编写代码,该如何导入 C 头文件的所有定义?

这看起来确实是一个巨大的障碍,但 Zig 将与 C 项目的集成作为一项优先任务,因此它提供了两种快速导入头文件的方法。

最快的办法就是直接调用 @cInclude(),它允许直接导入头文件,而第二种方法是使用编译器命令将 C 代码翻译成 Zig(这是 @cInclude() 在后台执行的操作)。

虽然第一种方案很直接,但在我们的案例中由于以下几个原因,这不是一个最优方案,其中最重要的原因是 Zig 是一种安全语言,头文件的自动翻译导致 Zig 假设所有指针都可能为 null,从而要求用户在执行任何操作之前必须进行明确的检查。

Zig 中的 null 值

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

在 Zig 中,可以为 null 的值包装在称为 Optionals 的泛型容器类型中。

可选类型正在变得非常普遍:SwiftKotlinRust 中都有,仅举几例。

当你有可选值(无论是指针还是其他任何东西)时,在访问它之前,你必须展开它。这样可以对任何潜在的 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 有一个专门用于共享库的构建命令

$ 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 以将其添加到列表中。

祝您玩得开心!