Tips and tricks in MRI Ruby development
This article is not maintained and is out of date. Please read the official contributing docs instead.
Introduction
Development inside MRI Ruby has a high learning curve. There’s a lot of development and debugging features that are barely documented on the internet, if at all. In this blog post, I attempt to document many useful development tricks that I’ve acquired while developing Ruby.
Building
This section is adapted from ko1/rubyhackchallenge.
We will use the following directory structure.
ruby/
: contains cloned git repository.ruby/build/
: Build directory (compilation files will be stored here).ruby/install/
: Install directory where Ruby will be installed.
Dependencies
If you are in a system with apt
(or apt-get
), run the following command to install dependencies required to build Ruby:
sudo apt install -y git ruby autoconf bison gcc make zlib1g-dev libffi-dev libreadline-dev libgdbm-dev libssl-dev
Or using Homebrew:
brew install gdbm gmp libffi openssl@1.1 zlib autoconf automake libtool readline
Configure
We first need to configure before we build. This step will set up and create the necessary files for building. Run the following command in the build
directory:
../configure --prefix=$PWD/../install --enable-shared
If you are using Homebrew, you will need to append the following flags
--with-openssl-dir="$(brew --prefix openssl)" --with-readline-dir="$(brew --prefix readline)" --disable-libedit
Miniruby
To build Ruby, you must first build miniruby. Miniruby is a version of Ruby with only the most essential features. To build miniruby, run this command inside the build
directory:
make miniruby -j
The -j
flag builds in parallel, thus speeding compilation.
You can now verify that miniruby has been successfully built by checking its version:
> ./miniruby -v
ruby 2.8.0dev (2020-05-29T14:06:46Z master 2ecfb88ee5) [x86_64-darwin19]
A handy command is make run
, which will run miniruby with the test.rb
script inside your ruby
directory. This is useful to quickly test your changes against a script (such as a reproduction for a bug).
You can also run make gdb
or make lldb
which is similar to make run
, but will start miniruby inside a debugger (gdb
or lldb
, respectively). Additionally, this will also include debugging tools specific to Ruby (see section on the rp
command for additional details).
Ruby
After miniruby is built, you can build Ruby using this command:
make install -j
Building Ruby requires a functioning build of miniruby, as building Ruby requires execution of Ruby scripts inside miniruby.
If you’re confident that your miniruby is functional, you can directly invoke make install -j
without make miniruby
because make install
will invoke make miniruby
as a dependency.
make install-nodoc -j
To save time, you can use the command above to install Ruby without parsing documentation.
You can now verify that miniruby has been successfully built by checking its version.
> ./ruby -v
ruby 2.8.0dev (2020-05-29T14:06:46Z master 2ecfb88ee5) [x86_64-darwin19]
Similar to make run
, you can use make runruby
to run test.rb
using Ruby (rather than miniruby).
Also similarly, you can run make gdb-ruby
or make lldb-ruby
to start Ruby in a debugger that runs test.rb
.
Debugging
macOS note
GDB does not play well with macOS, so LLDB is the recommended debugger to use on macOS.
You may encounter a prompt for your password saying “Developer Tools Access needs to take control of another process for debugging to continue.” every time you start LLDB. To permanently dismiss this prompt, run the following command:
sudo DevToolsSecurity --enable
Disabling compiler optimizations
You may want to update the Makefile
to disable compiler optimizations so that lines of code do not get optimized away (which may cause incorrect line numbers in the debugger). To do so, change the line optflags = -O3
to optflags = -O0
in the Makefile
of your build
directory.
Alternatively, if you want to disable compiler optimizations permanently, you can set these environment variables
debugflags="-g"
optflags="-O0"
RUBY_DEVEL="yes"
Make sure to reconfigure after setting the environment variables to ensure that the Makefile gets updated.
The rp
tool
Ruby provides the rp
tool that allows you to view and manipulate Ruby objects in the debugger. It’s loaded when you run make gdb
/ make gdb-ruby
or make lldb
/ make lldb-ruby
.
For example, if obj
is a pointer to a valid Ruby object, then we can do the following to view the object.
>> rp obj
T_ARRAY: [FROZEN] len=4 (ownership) capa=20
(const VALUE *) $1 = 0x0000000104ff8120 {
(VALUE) [0] = 0x0000000101826638
(VALUE) [1] = 0x00000001018265e8
(VALUE) [2] = 0x0000000101826598
(VALUE) [3] = 0x0000000101826548
}
We can see in this output that obj
is an array with 4 elements. This array is conveniently stored in $1
for us, let’s use it to analyze the first element in the array.
>> rp $1[0]
T_STRING: [FROZEN] (const char [10]) $3 = "USE_RGENGC"
We can see that the first element of this array is the string "USE_RGENGC"
(which is stored for us in $3
).
The scripts for this tool is located in .gdbinit
for GDB or misc/lldb_cruby.py
for LLDB.
Address Sanitizer (ASan)
ASan can be enabled to detect possible memory errors (such as using memory after freeing). ASan is also used a lot in the Ruby codebase manually, by poisoning regions of unused memory. When a poisoned region of memory is accessed (either read from or written to), ASan will crash and output the issue. We can only access a region of memory when we unpoison it. To enable ASan, add -fsanitize=address
to CFLAGS
in the Makefile
.
Visual Studio Code
We can use the built-in debugger in VSCode to make development easier. Debugging in VSCode allows you to set breakpoints, step through code, view variables, view call stack, and interact with the debugger, all directly inside the editor.
Building miniruby
In order to properly debug, we first need an up-to-date build of miniruby. Let’s create a task that builds miniruby. Open .vscode/tasks.json
and add the following to tasks
to create a task called makeMiniruby
. Note that you may need to change the command
if you use a different build directory.
{
"label": "makeMiniruby",
"type": "shell",
"command": "make -C ${workspaceRoot}/build miniruby"
}
Debugging with GDB
To debug with GDB inside VSCode add the following launch script to .vscode/launch.json
under configurations
. This will create a task called MINIRUBY - DEBUG
that you can run to debug. You may need to change some of the paths if your build
directory exists elsewhere.
{
"name": "MINIRUBY - DEBUG",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/miniruby",
"args": ["--", "../test.rb"],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"miDebuggerArgs": "-x .gdbinit",
"preLaunchTask": "makeMiniruby",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
]
}
Debugging with LLDB
To debug with LLDB inside VSCode add the following launch script to .vscode/launch.json
under configurations
. This will create a task called MINIRUBY - DEBUG
that you can run to debug. You may need to change some of the paths if your build
directory exists elsewhere.
{
"name": "MINIRUBY - DEBUG",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/build/miniruby",
"args": ["--", "../test.rb"],
"stopAtEntry": false,
"cwd": "${workspaceFolder}/build",
"environment": [],
"externalConsole": false,
"MIMode": "lldb",
"preLaunchTask": "makeMiniruby",
"setupCommands": [
{
"text": "command script import ../misc/lldb_cruby.py",
"ignoreFailures": false
}
]
}
Testing
Ruby has three test suites and a lot of tests.
make btest
: Bootstrap tests inruby/boostraptest/
. These tests serve as sanity checks for miniruby, so they are fast to run and not comprehensive.make test-all
: Run tests inruby/test/
. These tests are test-unit styled tests that run on Ruby (and not miniruby).make test-spec
: Run tests inruby/spec/
. These tests are spec styled tests that also run on Ruby.
make test-all
and make test-spec
together will run all Ruby tests and are very comprehensive (it will take about half an hour to an hour to run all tests).
Running a specific test
To run a specific test use:
make test-all TEST='test/ruby/test_foobar.rb -n test_that_i_want_to_run'
This will run test in file test/ruby/test_foobar.rb
called test_that_i_want_to_run
.
Running tests with debugger
To run tests with the debugger run:
make test-all RUNRUBYOPT=--debugger
Or for lldb:
make test-all RUNRUBYOPT=--debugger=lldb
Miscellaneous
IntelliSense errors in VSCode
You might encounter red squigglies in VSCode saying:
#include errors detected. Please update your includePath.
In this case, add ${workspaceFolder}/include/**
and ${workspaceFolder}/../build
to includePath
in .vscode/c_cpp_properties.json
. Your includePath
should look like this:
"includePath": [
"${workspaceFolder}/**",
"${workspaceFolder}/include/**",
"${workspaceFolder}/../build"
]