Open-Source Wikis

/

Ruby

/

Systems

/

I/O

ruby/ruby

I/O

io.c (~436 KB) is one of the largest source files in the tree. It implements the IO class hierarchy — File, BasicSocket ancestor, STDIN/STDOUT/STDERR, plus IO.pipe, IO.select, IO.popen, and the bridge to the OS file descriptor world. io_buffer.c adds IO::Buffer, a zero-copy byte buffer abstraction.

Purpose

  • Bridge Ruby strings, encodings, and Fiber Schedulers to OS file descriptors.
  • Implement non-blocking IO, autocomplete behaviour around encodings, and the read_nonblock/write_nonblock/wait_readable family.
  • Handle Windows quirks where file handles aren't pure POSIX descriptors.
  • Cooperate with the GVL so blocking IO doesn't stall every thread.

Files

File Purpose
io.c The IO class. ~436 KB / ~13,000 lines.
io.rb Ruby-level helpers compiled into the binary.
io_buffer.c IO::Buffer — pinned, encoding-agnostic byte buffer.
ext/io/console/ IO#raw, IO#getch, terminal control
ext/io/wait/ IO#wait_readable, IO#wait_writable, IO#ready?
ext/io/nonblock/ IO#nonblock=, IO#nonblock?
transcode.c Powers IO's automatic encoding conversion
scheduler.c Interface used by IO blocking points to defer to a Fiber Scheduler

The IO struct

typedef struct rb_io {
    int fd;                             /* OS file descriptor */
    FILE *stdio_file;                   /* optional stdio wrapper */
    int mode;                           /* read/write/append/sync flags */
    rb_pid_t pid;                       /* if popened */
    int lineno;                         /* $. for this IO */
    VALUE pathv;                        /* path name */
    VALUE write_lock;
    VALUE timeout;
    rb_io_buffer_t rbuf;                /* read buffer */
    rb_io_buffer_t wbuf;                /* write buffer */
    rb_econv_t *encoder;                /* encoding converter on write */
    rb_econv_t *readconv;               /* encoding converter on read */
    rb_io_encoding_t encs;              /* external + internal encodings */
} rb_io_t;

Defined in io.h. The buffers (rb_io_buffer_t) are simple byte arrays with read/write positions; the read buffer holds bytes consumed from the OS but not yet returned to Ruby, the write buffer holds bytes written by Ruby but not yet flushed.

Read/write pipeline

graph LR
    user[User: io.read] -->|len| ruby[rb_io_read]
    ruby -->|maybe transcode| readconv[readconv: enc -> str.encoding]
    readconv --> rbuf[rbuf: read buffer]
    rbuf -->|empty?| syscall[read syscall, GVL released]
    syscall -->|bytes| rbuf
    rbuf --> result["String (decoded)"]

    user2[User: io.write str] --> wbuf[wbuf: write buffer]
    wbuf -->|maybe transcode| encoder
    encoder -->|sync or full?| flush[write syscall, GVL released]
    flush --> os[OS]

For each public method:

  • IO#read(n) — fill rbuf from the FD as needed, decode through readconv if internal encoding is set, return a String.
  • IO#write(s) — encode s to external encoding, append to wbuf, flush if mode is sync or buffer is full.
  • IO#gets — search rbuf for the line separator ($/), refill if not found, return the line.

All blocking syscalls release the GVL via rb_io_blocking_region (a thin wrapper around rb_thread_call_without_gvl).

Non-blocking IO

IO#read_nonblock, IO#write_nonblock, IO#accept_nonblock, etc. set O_NONBLOCK on the FD if not already set, then call the syscall and translate EAGAIN/EWOULDBLOCK to either a Ruby exception (IO::WaitReadable/IO::WaitWritable) or a symbol (:wait_readable/:wait_writable) depending on :exception => false.

The IO::Wait* modules are mixed into the corresponding errors so user code can rescue:

begin
  buf = io.read_nonblock(4096)
rescue IO::WaitReadable
  IO.select([io])
  retry
end

Fiber Scheduler integration

When a Fiber Scheduler is installed (Fiber.set_scheduler(s)), blocking IO methods detour through it:

VALUE scheduler = rb_fiber_scheduler_current();
if (!NIL_P(scheduler)) {
    return rb_fiber_scheduler_io_wait(scheduler, io, RB_INT2NUM(events), timeout);
}

Each method (read, write, IO.select, Kernel#sleep, Process.wait) checks for a scheduler before blocking. If present, the current Fiber suspends until the scheduler resumes it. If absent, falls back to a normal blocking syscall (with GVL release).

This is what powers gems like async, evt, and similar event-loop implementations.

IO::Buffer

IO::Buffer (io_buffer.c, ~123 KB) is a typed view over a region of memory. It supports:

  • Backing: heap-allocated, mmap'd file, mmap'd anonymous, or external pointer.
  • Locking: pin or unpin to keep the GC from moving the buffer.
  • Slicing: zero-copy sub-buffers.
  • Typed reads/writes: get_value(:U32, offset), set_value(:F64, offset, val), with explicit endianness.
  • Direct IO: IO::Buffer#read(io, length, offset) writes straight into the buffer without an intermediate String.

Used by performance-sensitive networking code that wants to avoid string allocations.

IO.popen and process IO

IO.popen(cmd) forks a child, sets up pipes, and returns an IO connected to the child's stdin/stdout. Implementation in io.c::pipe_open. On Windows, it uses CreatePipe + CreateProcess instead.

IO.popen cooperates with the Fiber Scheduler's process_wait.

Windows quirks

On Windows, file descriptors aren't a clean POSIX concept. io.c deals with:

  • _open_osfhandle / _get_osfhandle to bridge between int fd and HANDLE.
  • Console mode toggling (raw/cooked) for terminals.
  • WSAStartup/WSACleanup for sockets.
  • Newline translation: text-mode IO converts \r\n\n.
  • Async IO via IOCP for some operations (selectively).

win32/win32.c provides the platform glue.

Standard streams

STDIN, STDOUT, STDERR are initialised at startup (io.c::Init_IO):

  • Their encodings come from Encoding.default_external and Encoding.default_internal.
  • They're synced with stdio so printf and Ruby puts interleave correctly.
  • On Windows, console mode is detected to pick UTF-8 vs the system codepage.

Common pitfalls

  • Encoding mismatch on read: an external encoding of Encoding::ASCII_8BIT returns binary; setting an internal encoding triggers transcoding on every read.
  • Forgetting to close: GC closes IOs eventually, but the FD is held until then. Use blocks (File.open(...) { |f| ... }) or ensure.
  • STDOUT.sync = false: buffered. Mixed with print and a crash, you can lose output. CRuby flushes on exit.
  • select with too many FDs: IO.select is select(2), capped at FD_SETSIZE. For many FDs, use IO::Buffer + scheduler or third-party nio4r.

Entry points for modification

  • New IO method: add a C function to io.c, register with rb_define_method in Init_IO. Add tests in test/ruby/test_io.rb and specs in spec/ruby/core/io/.
  • Buffering bug: io.c::io_fillbuf (read) and io.c::io_fwritev (write) are the central paths.
  • Scheduler hook: rb_io_wait is the canonical "block and check scheduler" function.
  • Encoding conversion bug: transcode.c for the converter; the IO side hooks in io.c::make_writeconv and io.c::make_readconv.

See encoding.md for the encoding system and fiber.md for the scheduler.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

I/O – Ruby wiki | Factory