こんにちは。
今日はCrystalのIOの実装について簡単に紹介していきます。
バッファリングまわりにも簡単に触れます。
Kernel#puts
の行く末を追う
もっとも馴染み深いメソッドであるKernel#puts
がどこにたどり着くのか追ってみましょう。
なんか前にだれかがやってた気がするけど資料みつけられなかったし、かなり前なのでちょっと変わっているだろうと思うのでやってみます。
Kernel#puts(*objects)
source: https://github.com/crystal-lang/crystal/blob/0.24.0/src/kernel.cr#L82
# 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
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
# 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
# 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
# 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
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
# 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
# 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
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
が呼び出されました!
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 さんです!お楽しみに!