CrystalのKernel#putsの行く末を追う ver:0.24.0

こんにちは。
今日はCrystalのIOの実装について簡単に紹介していきます。
バッファリングまわりにも簡単に触れます。

# `Kernel#puts`の行く末を追う

もっとも馴染み深いメソッドである`Kernel#puts`がどこにたどり着くのか追ってみましょう。

なんか前にだれかがやってた気がするけど資料みつけられなかったし、かなり前なのでちょっと変わっているだろうと思うのでやってみます。

## `Kernel#puts(*objects)`

source: https://github.com/crystal-lang/crystal/blob/0.24.0/src/kernel.cr#L82

```rb
# Prints objects to `STDOUT`, each followed by a newline.
#
# See also: `IO#puts`.
def puts(*objects) : Nil
  STDOUT.puts *objects
end
```

`STDOUT.puts` に移譲している。

## `STDOUT`

source: https://github.com/crystal-lang/crystal/blob/0.24.0/src/kernel.cr#L4

```rb
STDOUT = (IO::FileDescriptor.new(1, blocking: LibC.isatty(1) == 0)).tap { |f| f.flush_on_newline = true }
```

`STDOUT`は`IO::FileDescriptor` のインスタンスのようだ。
`IO::FileDescriptor#puts(*objects : _)` は無いので、親クラスの`IO#puts(*objects : _)`が呼ばれる。

## `IO#puts(*objects : _)`

source: https://github.com/crystal-lang/crystal/blob/0.24.0/src/io.cr#L265-L277

```rb
# Writes the given objects, each followed by a newline character.
#
# ```
# io = IO::Memory.new
# io.puts 1, '-', "Crystal"
# io.to_s # => "1\n-\nCrystal\n"
# ```
def puts(*objects : _) : Nil
  objects.each do |obj|
    puts obj
  end
  nil
end
```

引数毎に`IO#puts`を呼び出している。なんと、Crystalの型システムではここでオーバーロードも効くので文字列を渡した場合はオーバーロードされて `IO#puts(String)` が呼び出さるようだ。

## `IO#puts(String)`

source: https://github.com/crystal-lang/crystal/blob/0.24.0/src/io.cr#L225-L238

```rb
# Writes the given string to this `IO` followed by a newline character
# unless the string already ends with one.
#
# ```
# io = IO::Memory.new
# io.puts "hello\n"
# io.puts "world"
# io.to_s # => "hello\nworld\n"
# ```
def puts(string : String) : Nil
  self << string
  puts unless string.ends_with?('\n')
  nil
end

# Writes a newline character.
#
# ```
# io = IO::Memory.new
# io.puts
# io.to_s # => "\n" # ```
def puts : Nil
  print '\n'
  nil
end
```

`IO#<<(obj)`に移譲している。
引数なしの`puts`は改行コードを`IO#print`に渡しているが`IO#print`は`IO#<<(obj)`に移譲しているので結局そっちに行く。

## `IO::<<`

source: https://github.com/crystal-lang/crystal/blob/0.24.0/src/io.cr#L181-L194

```rb
# Writes the given object into this `IO`.
# This ends up calling `to_s(io)` on the object. #
# ```
# io = IO::Memory.new
# io << 1
# io << '-'
# io << "Crystal"
# io.to_s # => "1-Crystal"
# ```
def <<(obj) : self
  obj.to_s self
  self
end
```

`Object#to_s(IO)`に移譲される。abstract methodなので `String#to_s(IO)` を見てみる。

## `String#to_s(IO)`

source: https://github.com/crystal-lang/crystal/blob/0.24.0/src/string.cr#L4138-L4140

```rb
def to_s(io)
  io.write_utf8(to_slice)
end
```

つまり、文字列からスライスを作ってそれを `io.write_utf8` に渡すようだ。
`IO::FileDescriptor#write_utf8`はないので`IO#write_utf8`を呼ぶことになる。

## `IO#write_utf8(Bytes)`

source: https://github.com/crystal-lang/crystal/blob/0.24.0/src/io.cr#L482-L498

```rb
# Writes a slice of UTF-8 encoded bytes to this `IO`, using the current encoding.
def write_utf8(slice : Bytes)
  if encoder = encoder()
    encoder.write(self, slice)
  else
    write(slice)
  end
  nil
end

private def encoder
  if encoding = @encoding
    @encoder ||= Encoder.new(encoding)
  else
    nil
  end
end
```

`encoding` がセットされていれば `Encoder` 経由でwriteするようだ。
どのみち最終的に呼ばれるのは `write` なのでそちらを見よう。
`IO::FileDescriptor` は `IO::Buffered` を `include` しており、`write`はそこに定義されている。

## `IO::Buffered#write(Bytes)`

source: https://github.com/crystal-lang/crystal/blob/0.24.0/src/io/buffered.cr#L105-L138

```rb
# Buffered implementation of `IO#write(slice)`.
def write(slice : Bytes)
  check_open

  count = slice.size

  if sync?
    return unbuffered_write(slice)
  end

  if flush_on_newline?
    index = slice[0, count.to_i32].rindex('\n'.ord.to_u8)
    if index
      flush
      index += 1
      unbuffered_write slice[0, index]
      slice += index
      count -= index
    end
  end

  if count >= BUFFER_SIZE
    flush
    return unbuffered_write slice[0, count]
  end

  if count > BUFFER_SIZE - @out_count
    flush
  end

  slice.copy_to(out_buffer + @out_count, count)
  @out_count += count
  nil
end
```

元気よくバッファリングしている。
少々複雑で解説が大変なのでネタバレをすると最終的には `unbuffered_write` が呼び出されます。

`IO::Buffered` の実装の解説は要望が多ければ別の記事で紹介してもいいかも。

## `IO::FileDescriptor#unbuffered_write`

source: https://github.com/crystal-lang/crystal/blob/0.24.0/src/io/file_descriptor.cr#L186-L194

```rb
private def unbuffered_write(slice : Bytes)
  write_syscall_helper(slice, "Error writing file") do |slice|
    LibC.write(@fd, slice, slice.size).tap do |return_code|
      if return_code == -1 && Errno.value == Errno::EBADF
        raise IO::Error.new "File not open for writing"
      end
    end
  end
end
```

`write_syscall_helper` とかは防御機構なのでさておき、
めでたく `LibC.write` が呼び出されました! :tada: 

`LibC.write` はその名前から分かる通りlibcのwrite関数のbindingです。
Linux/UNIXの場合はシステムコールの `write(2)` が呼び出されると思います。

# まとめ

CrystalのIOの振る舞いは`IO`というabstract classで定義されています。
また、標準入出力はSTDIN/STDOUT/STDERRとして`Kernel` classに定義されており、それぞれが`IO::FileDescriptor`のインスタンスになっています。

`Kernel#puts(*objects)` -> `IO#puts(*objects : _)` -> `IO#puts(String)/IO#puts(obj)` といった深い呼び出しでもメソッドオーバーロードが行われて型に応じて呼び出されるメソッドが変わるようになっています。Crystalのオーバーロードがかなり強力なものであることがわかりました。
そして、`IO::FileDescriptor`は`IO::Buffered`をベースに実装されているため、バッファリング機能が実装されています。

`lib`によるFFIでLibCを触るところまでCrystalで書かれているのでなかなか実装が追いやすいことがおわかり頂けたかと思います。
ときにはこうやってCrystalの中のコードを追ってみるのも楽しいかと思います。

明日は @TobiasGSmollett さんです！お楽しみに！