Peter's Adventures in Ruby: Tips and tricks in MRI Ruby development

This is an article in a multi-part series called “Peter’s Adventures in Ruby”

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 in ruby/boostraptest/. These tests serve as sanity checks for miniruby, so they are fast to run and not comprehensive.
  • make test-all: Run tests in ruby/test/. These tests are test-unit styled tests that run on Ruby (and not miniruby).
  • make test-spec: Run tests in ruby/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"
]