NetBSDでname/valueリストを扱うlibnvを試してみる

[NetBSD Advent Calendar 2022](https://qiita.com/advent-calendar/2022/netbsd) 21日目の記事です。今日はNPF(NetBSD Packet Filter)のソースコード中に出てきた、 `libnv` の紹介を使用と思います。

## libnvとは？

[20日目の記事](https://qiita.com/furandon_pig/items/8fd923d445b1fba9b6e9)でNPFのソースコードを調べた際に、 `nvlist_add_string()` という関数が出てきました。例えば、[/usr/src/lib/libnpf/npf.c](https://github.com/NetBSD/src/blob/trunk/lib/libnpf/npf.c)にある以下のような処理です。

```c
 629 nl_ext_t *
 630 npf_ext_construct(const char *name)
 631 {
 ...
 639     nvlist_add_string(ext->ext_dict, "name", name);
 640     return ext;
 641 }
```

関数名に含まれている"nvlist"とは、そもそも一体何なのでしょうか？
`libnv` のソースコードは [/usr/src/sys/external/bsd/libnv/dist](https://github.com/NetBSD/src/tree/trunk/sys/external/bsd/libnv/dist) 以下に置かれています。

```sh
$ ls
CVS/                dnv.9               nv.9                nvlist.c
cnv.9               dnv.h               nv.h                nvlist_impl.h
cnv.h               dnvlist.c           nv_compat.h         nvpair.c
cnvlist.c           msgio.c             nv_impl.h           nvpair_impl.h
common_impl.h       msgio.h             nv_kern_netbsd.c
```

`nv.9` が `man 9 nv` で参照されるファイルのようです。なぜか `man 9 nv` でマニュアルが引けないのですが、 `mandoc` コマンドで整形したマニュアルにしてみます。

```sh
$ mandoc nv.9
NV(9)                      Kernel Developer's Manual                     NV(9)

NAME
     nvlist_create, nvlist_destroy, nvlist_error, nvlist_set_error,
     nvlist_empty, nvlist_flags, nvlist_exists, nvlist_free, nvlist_clone,
     nvlist_dump, nvlist_fdump, nvlist_size, nvlist_pack, nvlist_unpack,
     nvlist_send, nvlist_recv, nvlist_xfer, nvlist_in_array, nvlist_next,
     nvlist_add, nvlist_move, nvlist_get, nvlist_take, nvlist_append - library
     for name/value pairs

LIBRARY
     Name/value pairs library (libnv, -lnv)

...
DESCRIPTION
     The libnv library allows to easily manage name value pairs as well as
     send and receive them over sockets.  A group (list) of name value pairs
     is called an nvlist.  The API supports the following data types:
```

なるほど、どうやら `libnv` は `Name/value` のペアになったデータを扱うためのライブラリで、さらにソケット経由でデータをやりとりすることも可能なようですね。

## libnvはBSD系のOSなら利用できる？

NetBSDでは `man 9 nv` は存在していませんが、FreeBSDでは[libnv(9)](https://www.freebsd.org/cgi/man.cgi?query=libnv&apropos=0&sektion=0&manpath=FreeBSD+14.0-CURRENT&arch=default&format=html)でマニュアルを引くことができます。少なくとも、FreeBSDとNetBSDでは `libnv` が利用できるようです。

FreeBSDでは `/lib/libnv.so.0` でライブラリが提供されていますが、NetBSDにはこのライブラリが存在していません…。 `libnv` のソースコードは提供されているため、 `libnv` のソースコードを一緒にコンパイルする(アプリに直接組み込む)ことで利用できるはずです。

が、後述する `libnv` を使用したサンプルをコンパイルしようとすると、以下のエラーが発生します。

```sh
$ gcc \
    /usr/src/sys/external/bsd/libnv/dist/*.c \
    sample.c \
    -o sample \
    -I/usr/src/sys/external/bsd/libnv/dist/
ld: /tmp//cc8QVbnj.o: in function `main':
sample.c:(.text+0xc3): undefined reference to `nvlist_send'
ld: sample.c:(.text+0x11d): undefined reference to `nvlist_recv'
```

`nvlist_send()` という関数が見つからないというエラーになっています。ソースコードをgrepしてみると、この関数は `nvlist.c` で定義されています。

```sh
$ pushd /usr/src/sys/external/bsd/libnv/dist/
$ grep nvlist_send *.c
nv_kern_netbsd.c:nvlist_send_ioctl(int fd, unsigned long cmd, const nvlist_t *nvl)
nvlist.c:nvlist_send(int sock, const nvlist_t *nvl)
nvlist.c:       if (nvlist_send(sock, nvl) < 0) {
```

`nvlist_send()` がコンパイルされるための条件を見ると、 `_KERNEL` と `_STANDALONE` が「定義されていない」状態で、かつ `WITH_MSGIO` マクロが定義されている場合にコンパイルされます。

```sh
grep -B3 ^nvlist_send nvlist.c

#if !defined(_KERNEL) && !defined(_STANDALONE) && defined(WITH_MSGIO)
int
nvlist_send(int sock, const nvlist_t *nvl)
```

さて、この `WITH_MSGIO` マクロですが、NetBSDのソースツリーの中では `nvlist.c` にしか出てきません。つまり、そのままだと常に `nvlist_send()` が未定義になるという構成になっています。FreeBSDの `libnv` では、このマクロ自体が存在していないため、NetBSDで独自に追加されているようですが、現状だと意味をなしていない(むしろ `libnv` をユーザランドで利用する際の妨げになっている？)ように見えます。とりあえず、これ以上の深掘りは止めて、コンパイル時に `-DWITH_MSGIO` を指定する方法で対応してみます。

## libnvの簡単なサンプルを動かしてみる

[libnv(9)](https://www.freebsd.org/cgi/man.cgi?query=libnv&apropos=0&sektion=0&manpath=FreeBSD+14.0-CURRENT&arch=default&format=html)のマニュアルを参照しながら `libnv` の簡単なサンプルを作成してみます。基本的には以下の手順によりプロセス間でのデータ通信が行えます。

 * データ送出側での処理
   * `nvlist_create()` でname/valueリストを作成する。
   * `nvlist_add_string()` `nvlist_add_number()` 等で、任意のデータ型のname/valueアイテムを設定する。
   * `nvlist_send()` で相手側のプロセスにデータを送出する。
   * `nvlist_destroy()` で確保したリソースを開放する。
 * データ受信側での処理
   * `nvlist_recv()` でname/valueリストを受信する。
   * `nvlist_get_string()` `nvlist_get_number()` 等で値を取得する。値取得時には設定されているnameをキーに指定する。
   * `nvlist_destroy()` で確保したリソースを開放する。

さっそくこの手順をコードに起こしてみます。具体的な実装は以下になります。

```c
// libnvを使用して親・子プロセス間でデータをやり取りするサンプル。

#include <sys/socket.h>
#include <sys/types.h>
#include <sys/wait.h>

// 以下の場所にあるnv.hを参照する。
//   /usr/src/sys/external/bsd/libnv/dist/
#include <nv.h>

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main(int argc, char *argv[])
{
    int r;
    int socks[2];
    int status;
    pid_t pid;

    r = socketpair(PF_UNIX, SOCK_STREAM, 0, socks);
    if (r == -1) {
        fprintf(stderr, "Error: sockpair()\n");
        exit(1);
    }

    pid = fork();
    if (pid == -1) {
    } else if (pid != 0) {
        // 親プロセス(データ送信側)
        nvlist_t *nvl;

        close(socks[0]);

        nvl = nvlist_create(0);

        // name/valueの形式で値をセットする。
        nvlist_add_string(nvl, "message", "hello,world");
        nvlist_add_number(nvl, "value", (uint64_t)123456);

        // 文字列と数値を送出する。
        nvlist_send(socks[1], nvl);

        nvlist_destroy(nvl);

        waitpid(pid, &status, 0);
        fprintf(stderr, "waitpid status= %d\n", WIFEXITED(status));
    } else {
        // 子プロセス(データ受信側)
        nvlist_t *nvl;
        const char *message;
        uint64_t value;

        nvl = nvlist_recv(socks[0], 0);
        if (nvl == NULL) {
            fprintf(stderr, "Error: nvlist_recv()\n");
            exit(1);
        }

        // 受信したデータの取り出し。
        message = nvlist_get_string(nvl, "message");
        value = nvlist_get_number(nvl, "value");

        printf("message= '%s', value= %llu\n", message, value);

        nvlist_destroy(nvl);
        fprintf(stderr, "quit child process.\n");
    }

    return 0;
}
```

コンパイルとプログラムの実行は以下の手順で行います。親プロセスと子プロセスの間で、文字列("hello,world")と値( `123456` )が送受信されていることが確認できます。

```sh
$ gcc \
    -DWITH_MSGIO \
    /usr/src/sys/external/bsd/libnv/dist/*.c \
    sample.c \
    -o sample \
    -I/usr/src/sys/external/bsd/libnv/dist/
$
$ ./sample
message= 'hello,world', value= 123456
quit child process.
waitpid status= 1
```

## まとめ

`libnv` の紹介と簡単なサンプルを作成してみました。C言語ではプロセス間で任意の型のデータを送受信するのは地味に手間なので、こういったライブラリで手順が一般化されているのは助かります。また、今回の記事では触れませんでしたが、NPF(NetBSD Packet Filter)では、ユーザランドとカーネル空間でのデータ送受信にも利用されており、だいぶ汎用性のある機能となっています。