Ruby's 'puts' and 'p' methods are commonly used for debugging and output. For example, both return "hello", but what happens internally?
$ puts "hello"
hello
$ p "hello"
"hello"
This article explores the internals of 'puts' and 'p'. The investigation reveals that both functions call the write system call, but in different ways. Despite Ruby being written in C, it doesn't use printf for its standard output functionality.
MacBook Air M2 arm64
Running Ubuntu 24.04 on Docker
# Create Dockerfile and build
docker build -t ruby-syscall - <<'EOF'
FROM ubuntu:24.04
RUN apt-get update && \
apt-get install -y --no-install-recommends \
ruby-full gcc strace linux-tools-common && \
rm -rf /var/lib/apt/lists/*
WORKDIR /ws
EOF
# Enter container
echo 'puts "hello from puts"; p "hello from p"' > test.rb && \
docker run --privileged -it --rm -v "$PWD/test.rb":/ws/test.rb ruby-syscall
This article deals with system calls - mechanisms for invoking OS functionality. When high-level languages output text to stdout or control file I/O, system calls are invoked deep in the code.
The simple conceptual model shows that application code doesn't directly access kernel space, but interacts with the OS and hardware through system calls, acting as an interface layer.
We can use strace to monitor what system calls are made when Ruby executes puts and p:
strace -e write ruby test.rb
Both puts and p ultimately use the write system call, but they differ in their approach:
Interesting discoveries about Ruby's implementation:
When examining the system call traces, we can observe:
Understanding these internals helps with performance considerations:
This exploration reveals that Ruby's output methods are more sophisticated than they appear on the surface. By implementing custom output handling rather than relying on C's printf, Ruby maintains better control over object representation and output formatting. Understanding these system-level details helps developers write more efficient Ruby code and debug output-related issues more effectively.