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_readablefamily. - 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)— fillrbuffrom the FD as needed, decode throughreadconvif internal encoding is set, return aString.IO#write(s)— encodesto external encoding, append towbuf, flush if mode issyncor buffer is full.IO#gets— searchrbuffor 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
endFiber 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 intermediateString.
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_osfhandleto bridge betweenint fdandHANDLE.- Console mode toggling (raw/cooked) for terminals.
WSAStartup/WSACleanupfor sockets.- Newline translation: text-mode IO converts
\r\n↔\n. - Async IO via
IOCPfor 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_externalandEncoding.default_internal. - They're synced with
stdiosoprintfand Rubyputsinterleave 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_8BITreturns 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| ... }) orensure. STDOUT.sync = false: buffered. Mixed withprintand a crash, you can lose output. CRuby flushes on exit.selectwith too many FDs:IO.selectisselect(2), capped atFD_SETSIZE. For many FDs, useIO::Buffer+ scheduler or third-partynio4r.
Entry points for modification
- New IO method: add a C function to
io.c, register withrb_define_methodinInit_IO. Add tests intest/ruby/test_io.rband specs inspec/ruby/core/io/. - Buffering bug:
io.c::io_fillbuf(read) andio.c::io_fwritev(write) are the central paths. - Scheduler hook:
rb_io_waitis the canonical "block and check scheduler" function. - Encoding conversion bug:
transcode.cfor the converter; the IO side hooks inio.c::make_writeconvandio.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.