My MacOS setup for hacking on CRuby

JP Camara

I recently posted my docker setup for hacking on CRuby, which showed how I test Linux features when working on CRuby. But most of the time, I just build CRuby directly on MacOS.

The Building Ruby guide from ruby-lang.org is the most up-to-date guide on doing this, but I like to spell it out exactly in order of how I do it day-to-day. So this is for me more than anything, but you may find it helpful!

# /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
xcode-select --install
brew update
brew install openssl@3
brew install autoconf
brew install gperf
brew install libffi
brew install libyaml
brew install zlib
brew install gmp
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

export CONFIGURE_ARGS=""
for ext in openssl readline libyaml zlib; do
  CONFIGURE_ARGS="${CONFIGURE_ARGS} --with-$ext-dir=$(brew --prefix $ext)"
done

./autogen.sh
mkdir build && cd build
mkdir ./.rubies

../configure --prefix="/path/to/ruby/build/.rubies/ruby-master" --disable-install-doc --config-cache --enable-debug-env optflags="-O0 -fno-omit-frame-pointer" CFLAGS="-DRUBY_DEBUG -O0" --with-opt-dir=$(brew --prefix gmp):$(brew --prefix jemalloc)

make install

That’s pretty much everything I do when setting things up!

If you want to run YJIT in “dev” mode, you add --enable-yjit=dev to the configure call:

../configure --prefix="/path/to/ruby/build/.rubies/ruby-master" --disable-install-doc --config-cache --enable-debug-env optflags="-O0 -fno-omit-frame-pointer" CFLAGS="-DRUBY_DEBUG -O0" --with-opt-dir=$(brew --prefix gmp):$(brew --prefix jemalloc) --enable-yjit=dev

From here, the simplest way to run some code is to place a test.rb file in the root of the project and run it using make runruby. To run it in a debug mode, you can run make lldb-ruby.

Add a comment

Counting C method calls in CRuby

JP Camara

There is a central macro in CRuby, RUBY_VM_CHECK_INTS, which is a very hot path for the Ruby runtime. It’s an important part of how threads are managed, and it’s called constantly. I was curious just how often it was called, and it turned out CRuby comes with some handy debugging functionality for just this scenario.

Inside of debug_counter.h, I changed #define USE_DEBUG_COUNTER 0 to #define USE_DEBUG_COUNTER 1 and added this line later in that file:

RB_DEBUG_COUNTER(rb_vm_check_ints)

Then inside vm_core.h I updated RUBY_VM_CHECK_INTS to add a debug increment:

#define RUBY_VM_CHECK_INTS(ec) rb_vm_check_ints(ec)
static inline void
rb_vm_check_ints(rb_execution_context_t *ec)
{
    RB_DEBUG_COUNTER_INC(rb_vm_check_ints); // increment!

After that I ran the following simple Ruby program:

10_000.times {}

And this was printed after it ran:

[RUBY_DEBUG_COUNTER]    rb_vm_check_ints    21,055

Iterating a loop ten thousand times results in twenty thousand calls to RUBY_VM_CHECK_INTS, exactly what I was looking to measure!

I’d like to know the proper configuration to compile without having to manually modify USE_DEBUG_COUNTER in the header file. Maybe someone can comment and let me know how? It has something to do with CFLAGS, I think.

In the meantime, it’s simple enough to manually change.

Add a comment

My docker setup for hacking on CRuby

JP Camara

I run on MacOS, but I often want to test Linux behaviors when working on the CRuby implementation.

Here’s the Dockerfile I use:

FROM ubuntu:24.04

# Preventing dialog prompts when installing packages
ENV DEBIAN_FRONTEND=noninteractive

# Update and install basic build dependencies and Rust
RUN apt-get update && apt-get install -y \
    git \
    curl \
    build-essential \
    autoconf \
    libreadline-dev \
    libssl-dev \
    libyaml-dev \
    libncurses5-dev \
    zlib1g-dev \
    libffi-dev \
    bison \
    libgdbm-dev \
    libgdbm-compat-dev \
    libreadline6-dev \
    libssl-dev \
    libgmp-dev \
    liburing-dev \
    && apt-get clean && rm -rf /var/lib/apt/lists/*

# Install Rust via rustup
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y

RUN apt-get update && apt-get install -y ruby
RUN apt-get update && apt-get install -y gdb

# Add Rust to the PATH so the cargo and rustc commands are available
ENV PATH="/root/.cargo/bin:${PATH}"

# Create a directory for the Ruby source code
WORKDIR /usr/src/ruby

# Copy Ruby source code from your local directory
COPY . .

# This will be the default command when you run the container.
CMD [ "/bin/bash" ]

To run the Dockerfile, you can use the following two commands:

docker build -t ruby-source-build-env .
docker run -it --mount type=bind,src=.,target=/usr/src/ruby ruby-source-build-env

Based on our Dockerfile, docker run will open up a bash shell for you. From there, I run the following commands to build CRuby:

./autogen.sh
mkdir build && cd build
mkdir ./.rubies
../configure --prefix="/usr/src/ruby/build/.rubies/ruby-master" --disable-install-doc --config-cache --enable-debug-env optflags="-O0 -fno-omit-frame-pointer"
make install

We now have CRuby operating under Ubuntu Linux! From here, the simplest way to run some code is to place a test.rb file in the root of the project and run it using make runruby.

Add a comment

Calculating the largest known prime in Ruby

JP Camara

Looking to impress your Ruby friends by calculating the largest known prime, 2 ** 136_279_841-1?

On Ruby 3.4.0-preview2 and earlier, 2 ** 136_279_841-1 logs a warning and returns Infinity 😔:

2 ** 136_279_841-1
# warning: in a**b, b may be too big
# => Infinity

Thanks to @mametter, Ruby 3.4 will handle this calculation just fine! See Do not round a**b to infinity.

Knowing this, you excitedly use your ruby manager of choice to pull down ruby master:

rvm install ruby-head

You run ruby -e "puts 2 ** 136_279_841-1", and your excitement is slowly eroded. An hour into calculating, you terminate the command in frustration 😫.

Is @mametter a liar?!

As it turns out, there is critically important library you need for accelerating “Bignum” calculations: GMP, the GNU Multiple Precision Arithmetic Library. It’s even specifically mentioned in the CRuby guide to building ruby.

Without it, you can kiss your largest prime calculating dreams goodbye 👋.

You reinstall ruby head, making sure gmp is available

brew install gmp
rvm reinstall ruby-head --with-gmp-dir=$(brew --prefix gmp)

With a bit of hope in your heart, you try again:

ruby -e "puts 2 ** 136_279_841-1"

Success! @mametter was telling the truth!

Within around 5 seconds, your terminal is filled with a beautiful output of 41,024,320 digits. Your Ruby friends cheer and carry you off on their shoulders.

This was all inspired by Matz’s keynote at RubyConf 2024 - where he mentioned that Ruby 3.4 can now calculate the largest known prime. For fun, I tried it on my mac and just let it keep running - 2 hours later, it was still running! I’d never heard of GMP, but now I know!

Add a comment