Ruby言語内での FiberScheduler#address_resolve の使われ方

## この記事について

Ruby3.x にて使用可能な `FiberScheduler#address_resolve`を自前実装している。

https://github.com/kudojp/async_scheduler/pull/23

このメソッドが返す値がRuby言語の中でどのように使用されるのかを確認したい。


## `FiberScheduler#address_resolve`について

以下の通り。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534442/50a0e7c3-f25c-4d88-8834-f2124686ed34.png)

https://docs.ruby-lang.org/en/3.1/Fiber/SchedulerInterface.html#method-i-address_resolve

## 調査内容

自分が実装するスケジューラにおいて、`FiberScheduler#address_resolve`が、`["1.1.1.1"]`を返すように実装してみた。

```ruby
class AsyncScheduler
  def address_resolve
    ["1.1.1.1"]
  end
end
```

すると、このスケジューラを利用した`Socket.getaddrinfo("www.google.com", 443)`は、

```ruby
Fiber.set_scheduler AsyncScheduler.new
Fiber.schedule do
  Socket.getaddrinfo("www.google.com", 443) ## この箇所
end
```

以下の値を返すようになった。

```ruby
[
  ["AF_INET", 443, "1.1.1.1", "1.1.1.1", 2, 1, 6],
  ["AF_INET", 443, "1.1.1.1", "1.1.1.1", 2, 2, 17],
  ["AF_INET", 443, "1.1.1.1", "1.1.1.1", 2, 3, 0]
]
```

以下2点を知りたい。

1. `2, 1, 6`, `2, 2, 17`, `2, 3, 0`は何を意味するのか？
1. Ruby内部のどういった処理によりこれらの３つの値が返っているのか？


## 調査(1) `2, 1, 6`, `2, 2, 17`, `2, 3, 0`は何を意味するのか？

> `singleton method Socket.getaddrinfo`
> アドレス情報とは7つの要素からなる次の形の配列です。
> 
> 第0要素 - アドレスファミリー (String)
第1要素 - ポート番号 (Integer)
第2要素 - ホスト名 (String)
第3要素 - アドレス (String)
第4要素 - アドレスファミリーに対応する Integer
第5要素 - ソケットタイプに対応する Integer
第6要素 - プロトコルに対応する Integer
> https://docs.ruby-lang.org/ja/3.1/method/Socket/s/getaddrinfo.html

つまり、

| | [address family](https://www.iana.org/assignments/address-family-numbers/address-family-numbers.xhtml)| [socket type](https://www.ibm.com/docs/ja/aix/7.3?topic=protocols-socket-types) | [protocol](https://learn.microsoft.com/ja-jp/windows/win32/winsock/socket-options)|
|-|-|-|-|
| `2, 1, 6` | `AF_INET`(IPv4)| `SOCK_STREAM` | `IPPROTO_TCP`|
| `2, 2, 17`| `AF_INET`(IPv4)| `SOCK_DGRAM`  | `IPPROTO_UDP` |
| `2, 3, 0` | `AF_INET`(IPv4)| `SOCK_RAW`    | `IPPROTO_IP`|



<details>
<summary>Socket::Constants</summary>

https://docs.ruby-lang.org/ja/3.1/class/Socket.html

- Socket::Constants::AF_INET = 2
- Socket::Constants::SOCK_STREAM = 1
- Socket::Constants::SOCK_DGRAM = 2
- Socket::Constants::SOCK_RAW = 3

- Socket::Constants::IPPROTO_IP = 0
- Socket::IPPROTO_TCP = 6
- Socket::Constants::IPPROTO_UDP = 17

</details>


なお、`SOCK_RAW`の説明は以下である。

> `SOCK_RAW`
> 内部ネットワーク・プロトコルおよびインターフェースへのアクセスを提供します。 このタイプのソケットは、root ユーザー権限を持つユーザー、または CAP_NUMA_ATTACH 機能を持つ非 root ユーザーのみが使用できます。 (非 root ロー・ソケット・アクセスの場合、 chuser コマンドは CAP_PROPAGATEとともに CAP_NUMA_ATTACH 機能を割り当てます。 詳しくは、 chuser コマンドを参照してください。)
>
> ロー・ソケットを使用すると、アプリケーションは下位レベルの通信プロトコルに直接アクセスできます。 ロー・ソケットは、通常のインターフェースを介して直接アクセスできないプロトコル機能を利用したい場合や、既存の低レベル・プロトコルの上に新しいプロトコルを構築したい場合に、上級者を対象としています。
> 
> ロー・ソケットは通常、データグラム指向ですが、正確な特性は、プロトコルによって提供されるインターフェースによって異なります。
> https://www.ibm.com/docs/ja/aix/7.3?topic=protocols-socket-types


## 調査(2) Ruby内部のどういった処理によりこれらの３つの値が返っているのか？

https://github.com/ruby/ruby を読む。


折角なので、手元にRubyのソースコードを落としてきて、lldbでブレイクポイントを仕掛けてみる。


https://github.com/ruby/ruby/blob/master/doc/contributing/building_ruby.md


に従う。

```ruby_repo/test.rb
require 'socket'
require '/Users/daiki-kudo/repos/nonblocking-resolv/lib/nonblocking/resolv'
require '/Users/daiki-kudo/repos/async_scheduler/lib/async_scheduler'

scheduler = AsyncScheduler::Scheduler.new
Fiber.set_scheduler scheduler
x = nil
Fiber.schedule do
  x = Socket.getaddrinfo("www.google.com", 443) ## この箇所
end
scheduler.close
print x
```

$`make lldb-ruby`で実行する。
($`make lldb`では、実行不可だった。外部gemが使用できないとどこかで読んだ記憶がある。)

```python
$ make runruby
RUBY_ON_BUG='gdb -x ../.gdbinit -p' ./miniruby -I../lib -I. -I.ext/common  ../tool/runruby.rb --extout=.ext  -- --disable-gems  ../test.rb 
[["AF_INET", 443, "1.1.1.1", "1.1.1.1", 2, 1, 6],
 ["AF_INET", 443, "1.1.1.1", "1.1.1.1", 2, 2, 17],
 ["AF_INET", 443, "1.1.1.1", "1.1.1.1", 2, 3, 0]]%
```

さて、とりあえず、`FiberScheduler#address_resolve`を呼び出しているCレイヤーの [scheduler.c#rb_fiber_scheduler_address_resolve](https://github.com/kudojp/ruby/blob/master-kudojp/scheduler.c#L671)にブレイクポイントを設置する。ここで、コールスタックを見て、どのような順番でメソッド呼び出しがされているかを調べる。

$ make lldb-ruby
```c
(lldb) br set --name rb_fiber_scheduler_address_resolve
Breakpoint 1: where = ruby`rb_fiber_scheduler_address_resolve + 22 at scheduler.c:673:25, address = 0x00000001001c2746

(lldb) r
Process 32213 launched: '/Users/daiki-kudo/repos/ruby/build/ruby' (x86_64)
Process 32213 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x00000001001c2746 ruby`rb_fiber_scheduler_address_resolve(scheduler=4326338320, hostname=4376285200) at scheduler.c:673:25
   670 	VALUE
   671 	rb_fiber_scheduler_address_resolve(VALUE scheduler, VALUE hostname)
   672 	{
-> 673 	    VALUE arguments[] = {
   674 	        hostname
   675 	    };
   676 	

(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
  * frame #0: 0x00000001001c2746 ruby`rb_fiber_scheduler_address_resolve(scheduler=4326338320, hostname=4376285200) at scheduler.c:673:25
    frame #1: 0x0000000104c8df66 socket.bundle`rsock_getaddrinfo [inlined] rb_scheduler_getaddrinfo(scheduler=<unavailable>, host=4376285200, service=<unavailable>, hints=0x0000000104c1fa70, res=<unavailable>) at raddrinfo.c:478:26
    frame #2: 0x0000000104c8df51 socket.bundle`rsock_getaddrinfo(host=4376285200, port=887, hints=0x0000000104c1fa70, socktype_hack=0) at raddrinfo.c:545:21
    frame #3: 0x0000000104c85c94 socket.bundle`sock_s_getaddrinfo(argc=<unavailable>, argv=<unavailable>, _=<unavailable>) at socket.c:1186:11
    frame #4: 0x0000000100255c64 ruby`vm_call_cfunc_with_frame_(ec=0x0000000104e05ed0, reg_cfp=0x0000000104c3ff90, calling=<unavailable>, argc=2, argv=0x0000000104c20038, stack_bottom=0x0000000104c20030) at vm_insnhelper.c:3468:11
    frame #5: 0x000000010023a431 ruby`vm_exec_core [inlined] vm_sendish(ec=0x0000000104e05ed0, reg_cfp=<unavailable>, cd=<unavailable>, block_handler=0, method_explorer=mexp_search_method) at vm_insnhelper.c:5546:15
    frame #6: 0x000000010023a374 ruby`vm_exec_core(ec=0x0000000104e05ed0) at insns.def:835:11
    frame #7: 0x0000000100236b92 ruby`rb_vm_exec(ec=0x0000000104e05ed0) at vm.c:2406:22
    frame #8: 0x000000010024ab7f ruby`vm_invoke_proc [inlined] invoke_block(ec=0x0000000104e05ed0, iseq=0x0000000101e04578, self=4301835640, captured=<unavailable>, cref=0x0000000000000000, type=<unavailable>, opt_pc=<unavailable>) at vm.c:1441:12
    frame #9: 0x000000010024aa7e ruby`vm_invoke_proc [inlined] invoke_iseq_block_from_c(ec=0x0000000104e05ed0, captured=<unavailable>, self=4301835640, argc=<unavailable>, argv=<unavailable>, kw_splat=<unavailable>, passed_block_handler=<unavailable>, cref=0x0000000000000000, is_lambda=<unavailable>, me=0x0000000000000000) at vm.c:1511:16
    frame #10: 0x000000010024a91f ruby`vm_invoke_proc [inlined] invoke_block_from_c_proc(ec=<unavailable>, proc=<unavailable>, self=<unavailable>, argc=<unavailable>, argv=<unavailable>, kw_splat=<unavailable>, passed_block_handler=<unavailable>, is_lambda=<unavailable>, me=0x0000000000000000) at vm.c:1609:16
    frame #11: 0x000000010024a7fd ruby`vm_invoke_proc(ec=0x0000000104e05ed0, proc=<unavailable>, self=4301835640, argc=<unavailable>, argv=<unavailable>, kw_splat=<unavailable>, passed_block_handler=0) at vm.c:1639:12
    frame #12: 0x000000010024a79a ruby`rb_vm_invoke_proc(ec=<unavailable>, proc=<unavailable>, argc=<unavailable>, argv=<unavailable>, kw_splat=<unavailable>, passed_block_handler=<unavailable>) at vm.c:1660:16
    frame #13: 0x0000000100078d20 ruby`rb_fiber_start(fiber=0x0000000104e05e80) at cont.c:2525:23
    frame #14: 0x000000010007b8c1 ruby`fiber_entry(from=<unavailable>, to=<unavailable>) at cont.c:843:5
```

つまり、コールスタックは上から順に
- scheduler.c:673 [#rb_fiber_scheduler_address_resolve](https://github.com/kudojp/ruby/blob/master-kudojp/scheduler.c#L670-L678)
- raddrinfo.c:478 [#rb_scheduler_getaddrinfo](https://github.com/kudojp/ruby/blob/master-kudojp/ext/socket/raddrinfo.c#L467-L515)
- raddrinfo.c:545 [#rsock_getaddrinfo](https://github.com/kudojp/ruby/blob/master-kudojp/ext/socket/raddrinfo.c#L517-L580)
- socket.c:1186 [#sock_s_getaddrinfo](https://github.com/kudojp/ruby/blob/master-kudojp/ext/socket/socket.c#L1160-L1191)


ここで、[raddrinfo.c#rb_scheduler_getaddrinfo](https://github.com/kudojp/ruby/blob/master-kudojp/ext/socket/raddrinfo.c#L467-L515) を抜粋すると、以下。
`FiberScheduler#address_resolve` から得られたIPアドレスの配列(今回は `["1.1.1.1"]`)に対してループを周している。
それぞれについて、`#numeric_getaddrinfo` を呼び出して得られた addrinfoのLinkedListを、同じくLinkedListである`(*res)->ai`の末尾にくっつけている。


```ext/socket/addrinfo.h
static int
rb_scheduler_getaddrinfo(VALUE scheduler, VALUE host, const char *service,
    const struct addrinfo *hints, struct rb_addrinfo **res)
{
    long i, len;
    struct addrinfo *ai, *ai_tail = NULL;

    // FiberScheduler#address_resolve から得られたIPアドレスの配列
    ip_addresses_array = rb_fiber_scheduler_address_resolve(scheduler, host);
    len = RARRAY_LEN(ip_addresses_array);

    for(i=0; i<len; i++) {
        ip_address = rb_ary_entry(ip_addresses_array, i);
        hostp = host_str(ip_address, _hbuf, sizeof(_hbuf), &_additional_flags);

        // ai には、address_info構造体のLinkedListの先頭が代入される。
        // 後述のように、このLinkedListは、SOCK_STREAM, SOCK_DGRAM, SOCK_RAWに対応する３つの要素を持つ。
        error = numeric_getaddrinfo(hostp, service, hints, &ai);

        if (!res_allocated) {
            res_allocated = 1;
            *res = (struct rb_addrinfo *)xmalloc(sizeof(struct rb_addrinfo));
            (*res)->allocated_by_malloc = 1;
            (*res)->ai = ai; // res に address_info構造体のLinkedListの先頭 を代入
            ai_tail = ai;
        } else {
            while (ai_tail->ai_next) {
                ai_tail = ai_tail->ai_next;
            }
            ai_tail->ai_next = ai;　// LinkedListの末尾に要素を追加
            ai_tail = ai;
        }
    }
```

<details>
<summary>addrinfo構造体の型</summary>

```ext/socket/addrinfo.h
struct addrinfo {
	int	ai_flags;	/* AI_PASSIVE, AI_CANONNAME */
	int	ai_family;	/* PF_xxx */
	int	ai_socktype;	/* SOCK_xxx */
	int	ai_protocol;	/* 0 or IPPROTO_xxx for IPv4 and IPv6 */
	size_t	ai_addrlen;	/* length of ai_addr */
	char	*ai_canonname;	/* canonical name for hostname */
	struct sockaddr *ai_addr;	/* binary address */
	struct addrinfo *ai_next;	/* next structure in linked list */
};
```

</details>



[raddrinfo.c#numeric_getaddrinfo](https://github.com/kudojp/ruby/blob/master-kudojp/ext/socket/raddrinfo.c#L202-L285)を抜粋すると以下。
確かに、`SOCK_STREAM`, `SOCK_DGRAM`, `SOCK_RAW`の3種類がlistとして定義されており、ループを回して、これらに対応する[address_info構造体](ext/socket/addrinfo.h)からなるLinkedListが`ai`に代入されている。

```ext/socket/raddrinfo.c
static int
numeric_getaddrinfo(const char *node, const char *service,
        const struct addrinfo *hints,
        struct addrinfo **res)
{
        static const struct {
            int socktype;
            int protocol;
        } list[] = {
            { SOCK_STREAM, IPPROTO_TCP }, // ここ!!!
            { SOCK_DGRAM, IPPROTO_UDP },
            { SOCK_RAW, 0 }
        };

        for (i = numberof(list)-1; 0 <= i; i--) {
            if ((hint_socktype == 0 || hint_socktype == list[i].socktype) &&
                (hint_protocol == 0 || list[i].protocol == 0 || hint_protocol == list[i].protocol)) {
                // ai  は、LinkedListの先頭のNode
                // ai0 は、今から新しく先頭に挿入するNode
                struct addrinfo *ai0 = xcalloc(1, sizeof(struct addrinfo));                struct sockaddr_in *sa = xmalloc(sizeof(struct sockaddr_in));
                INIT_SOCKADDR_IN(sa, sizeof(struct sockaddr_in));
                memcpy(&sa->sin_addr, ipv4addr, sizeof(ipv4addr));
                sa->sin_port = htons(port);
                ai0->ai_family = PF_INET;
                ai0->ai_socktype = list[i].socktype;
                ai0->ai_protocol = hint_protocol ? hint_protocol : list[i].protocol;
                ai0->ai_addrlen = sizeof(struct sockaddr_in);
                ai0->ai_addr = (struct sockaddr *)sa;
                ai0->ai_canonname = NULL;
                // ai0を先頭に挿入
                ai0->ai_next = ai;
                ai = ai0;
            }
        }

        if (ai) {
            *res = ai;
            return 0;
        }
}

```